Skip to content

Python: Support archive-type MCP skills (source, toolbox, sample)#7121

Open
giles17 wants to merge 4 commits into
microsoft:mainfrom
giles17:mcp-archive-skills
Open

Python: Support archive-type MCP skills (source, toolbox, sample)#7121
giles17 wants to merge 4 commits into
microsoft:mainfrom
giles17:mcp-archive-skills

Conversation

@giles17

@giles17 giles17 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

The MCP skills source previously discovered only skill-md index entries, where a skill's SKILL.md and sibling resources are fetched on demand from the server. The Agent Skills discovery spec (SEP-2640) also defines archive entries, where a skill is distributed as a single packaged archive (ZIP / TAR / gzip-compressed TAR) that unpacks into the skill's namespace. Without archive support, skills published in that format are silently unusable.

This change adds archive-type skill support end to end: the core MCPSkillsSource downloads and safely unpacks archives and serves them like file-based skills (never executing MCP-delivered scripts); FoundryToolbox.as_skills_provider() exposes the archive knobs so a hosted toolbox agent can configure extraction; and the toolbox MCP skills sample is updated to demonstrate the full advertise → load → read-resources flow with an archive skill. It brings the Python side to parity with the .NET implementation.

Description & Review Guide

  • What are the major changes?

    • Core (agent-framework-core)MCPSkillsSource now dispatches skill://index.json entries by their type (case-insensitive): skill-md (existing, fetched on demand) and archive (new). Entries whose type has no handler (e.g. mcp-resource-template) are skipped.
      • A private _ArchiveEntryLoader downloads each archive resource, extracts it into {dir}/{skill-name}/, prunes directories the server no longer advertises, and delegates discovery to an internal FileSkillsSource created with no script extensions and no runner, so bundled scripts surface as read-only resources only and are never runnable.
      • Hardened stdlib extraction (zipfile/tarfile/gzip): path-traversal ("zip-slip") guard, non-regular TAR member skipping (symlinks/hardlinks/devices), and file-count / total-uncompressed-size / download-size limits (decompression-bomb defense). Format detection uses magic bytes, then media type, then URL suffix.
      • Archive behavior is configured via archive_* constructor kwargs (archive_skills_directory, archive_resource_extensions, archive_resource_search_depth, archive_max_file_count, archive_max_size_bytes, archive_max_uncompressed_size_bytes) — Python uses plain kwargs rather than a *Options object as in .NET. Refresh/caching is provided by wrapping in CachingSkillsSource, consistent with how every other Python skill source composes caching.
      • Fixes FileSkillsSource to treat None extensions as "use defaults" and an empty tuple as "discover none" (an empty tuple previously fell back to defaults) — this is what lets the archive loader disable script discovery.
    • Hosting (agent-framework-foundry-hosting)FoundryToolbox.as_skills_provider() now forwards the archive_* options to the underlying MCPSkillsSource. Only explicitly-set options are forwarded, so unset ones keep the MCPSkillsSource defaults. This lets a hosted toolbox agent redirect archive extraction to a writable directory (the default is under the cwd, which may be read-only in a container).
    • Sample04-hosting/.../12_foundry_toolbox_mcp_skills now demonstrates all three progressive-disclosure stages with an archive skill: escalation-policy ships a references/refund-matrix.md resource and is uploaded as a ZIP archive, while support-style stays a single SKILL.md. main.py disables load_skill/read_skill_resource approval (the Responses host runs without an AgentSession) and points archive extraction at a temp directory. README, toolbox.yaml, and ignore files are updated to match.
  • What is the impact of these changes?

    • Additive and backwards compatible. skill-md discovery is unchanged, MCPSkillsSource(client=...) keeps working, and the new as_skills_provider() archive kwargs all default to "unset" (i.e. MCPSkillsSource defaults). Archive-bundled scripts are surfaced as readable resources only; the inner file source is created with no allowed script extensions and no script runner, so they are never discovered as runnable scripts.
    • Verified end to end against a live Foundry project: an archive skill uploaded to a toolbox is discovered, unpacked, and its bundled resource is read on demand (advertise → load → read resources).
  • What do you want reviewers to focus on?

    • The extraction hardening (_resolve_archive_destination, _copy_stream_with_limit, non-regular TAR member skipping) and the "scripts are never runnable" guarantee via script_extensions=() + no runner.
    • Whether forwarding only explicitly-set archive options (rather than duplicating MCPSkillsSource defaults) on as_skills_provider() reads well.

Related Issue

Fixes #6088

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Add `archive`-type skill support to `MCPSkillsSource` so an MCP server can
advertise packaged skills (ZIP / TAR / gzip-compressed TAR) that are
downloaded, safely unpacked to a local directory, and served like file-based
skills, while keeping the guarantee that MCP-delivered scripts are never
executed.

- Dispatch `skill://index.json` entries by `type`: `skill-md` (existing,
  fetched on demand) and `archive` (new). Unknown types are skipped.
- `_ArchiveEntryLoader` downloads, extracts, and prunes archive skills and
  delegates discovery to an internal `FileSkillsSource` created with no
  script extensions and no runner, so bundled scripts surface as read-only
  resources only.
- Hardened stdlib extraction: path-traversal (zip-slip) guard, non-regular
  TAR member skipping, and file-count / uncompressed-size / download-size
  limits.
- Configure via `archive_*` constructor kwargs (no options object, per Python
  conventions); use `CachingSkillsSource` for refresh rather than a source
  level refresh interval.
- Fix `FileSkillsSource` to treat `None` extensions as "use defaults" and an
  empty tuple as "discover none" (an empty tuple previously fell back to
  defaults).

Port of .NET PR microsoft#6631.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e358a4e-538f-46be-8c58-128b6182352d
Copilot AI review requested due to automatic review settings July 14, 2026 18:39
@giles17 giles17 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Jul 14, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 91%

✓ Correctness

The production code changes are well-structured and correct. The archive extraction hardening (path-traversal guards, link skipping, size/count limits) is sound. The FileSkillsSource None vs empty-tuple fix is clean and well-tested. The _mcp_first_blob helper, format detection, and archive entry lifecycle are all logically correct. The only issue found is in AGENTS.md where the SkillsSource decorators bullet point has its entire second half duplicated verbatim (from SkillsSourceContext (frozen) carries... onward), roughly doubling the paragraph length with identical content.

✓ Security Reliability

The archive extraction implementation is well-hardened from a security perspective. Path-traversal ('zip-slip') is guarded by both segment-level '..' checks and a defense-in-depth _is_path_within_directory call. TAR symlinks/hardlinks/devices are correctly skipped via member.isreg(). Decompression-bomb defense uses actual byte counting in _copy_stream_with_limit rather than trusting archive metadata. The 'scripts are never runnable' guarantee is enforced by passing script_extensions=() and script_runner=None to the internal FileSkillsSource, and the FileSkillsSource None-vs-empty-tuple fix correctly differentiates 'use defaults' from 'discover none'. Download size, file count, and uncompressed size limits are all enforced. No blocking security or reliability issues were found.

✓ Test Coverage

The PR adds substantial test coverage for the new archive-type MCP skill support, including integration tests through MCPSkillsSource (ZIP/TAR/TAR.GZ discovery, script non-runnability, pruning, oversized/invalid/unsupported archives, mixed entries) and unit tests for extraction hardening (format detection, path-traversal rejection, file-count/size limits, symlink skipping). Two meaningful test gaps exist: (1) the semantic change to FileSkillsSource where empty-tuple script_extensions/resource_extensions now means 'discover none' instead of falling back to defaults has no direct unit test in test_skills.py — it's only indirectly validated through test_bundled_script_is_never_runnable; and (2) the _mcp_first_blob helper's data-URI prefix stripping and invalid-base64 error handling paths have no test coverage.

✓ Failure Modes

The archive-type MCP skill support is well-implemented with solid extraction hardening (path-traversal guards, decompression-bomb limits, symlink skipping) and proper cleanup of partial extractions. The "scripts are never runnable" invariant is correctly enforced via script_extensions=() and no runner. Error handling throughout the archive loader is thorough — download failures, format detection failures, and extraction errors are all caught, logged, and result in the skill being skipped. The FileSkillsSource None vs empty-tuple semantics fix is clean and backwards-compatible. No blocking failure-mode issues were found.

✓ Design Approach

The archive support is close, but I found one design-level regression: archive resource reads now swallow all non-not-found failures and silently drop the skill. That breaks the existing MCPSkillsSource/MCPSkill error-handling contract in this repo and can turn transient/auth/server failures into successful-but-incomplete refreshes.


Automated review by giles17's agents

Comment thread python/packages/core/agent_framework/_skills.py Outdated
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _skills.py13417794%321, 591, 1109, 1124, 1126–1127, 1493–1494, 1736, 1765, 2277, 2466, 2981–2982, 3079, 3087, 3092, 3095, 3100, 3120, 3132, 3137, 3232, 3240, 3245, 3248, 3253, 3273, 3282, 3287, 3553–3554, 4010, 4013, 4015, 4017, 4134, 4361, 4436, 4451, 4484, 4488, 4492, 4606–4607, 4611–4612, 4644, 4647, 4651, 4666–4667, 4669–4670, 4693–4695, 4706–4707, 4715–4716, 4723, 4725–4727, 4734–4735, 4740–4741, 4751, 4753, 4946–4947, 4969–4970, 4977–4978
packages/foundry_hosting/agent_framework_foundry_hosting
   _toolbox.py76494%292, 296, 298, 300
TOTAL44730528588% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
8998 33 💤 0 ❌ 0 🔥 2m 1s ⏱️

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Only swallow "resource not found" MCP errors when downloading an archive
resource; re-raise every other error (auth failure, INTERNAL_ERROR,
connection drop, timeout) so a transient transport failure is not silently
turned into a missing skill. This matches the existing failure model used by
`_try_read_index` and `MCPSkill.get_resource`, and avoids a failed
`CachingSkillsSource` refresh overwriting a previously cached list with a
partial result.

Add tests asserting archive-download INTERNAL_ERROR and ConnectionError
propagate out of `get_skills`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e358a4e-538f-46be-8c58-128b6182352d
@giles17 giles17 marked this pull request as ready for review July 14, 2026 21:12

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 82% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by giles17's agents

…mple

- FoundryToolbox.as_skills_provider() now forwards the MCPSkillsSource archive
  options (archive_skills_directory, archive_resource_extensions,
  archive_resource_search_depth, archive_max_file_count, archive_max_size_bytes,
  archive_max_uncompressed_size_bytes). Only explicitly-set options are
  forwarded so unset ones keep the MCPSkillsSource defaults. This lets a hosted
  toolbox agent redirect archive extraction to a writable directory (the default
  is under the cwd, which may be read-only in a container).
- Add unit tests covering default (no options forwarded) and override forwarding.
- Update the 12_foundry_toolbox_mcp_skills sample to demonstrate all three
  progressive-disclosure stages with an archive skill: escalation-policy now
  ships a references/refund-matrix.md resource and is uploaded as a ZIP archive;
  main.py disables load_skill and read_skill_resource approval and points
  archive extraction at a temp directory. README, toolbox.yaml, and ignore files
  updated accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f14f83d-1868-45c1-be1a-12f49a58ac36
@giles17 giles17 changed the title Python: Support archive-type MCP skills in MCPSkillsSource Python: Support archive-type MCP skills (source, toolbox, sample) Jul 15, 2026
@giles17 giles17 marked this pull request as draft July 15, 2026 02:10
@giles17 giles17 marked this pull request as ready for review July 15, 2026 02:10
@giles17 giles17 marked this pull request as draft July 15, 2026 02:14
Cast provider._source to _FoundryToolboxSkillsSource before accessing the
private _archive_options, so the ty checker (which runs over tests) resolves
the concrete type instead of the SkillsSource base. Replaces the mypy-style
type: ignore that ty did not honor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f14f83d-1868-45c1-be1a-12f49a58ac36
@giles17 giles17 marked this pull request as ready for review July 15, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: Support MCP skills of archive type

2 participants