Skip to content

Python: Add MCP-based skills support (skill-md type) - #6149

Closed
semenshi-m (semenshi) wants to merge 3 commits into
microsoft:mainfrom
semenshi:python/add-mcp-skills-source
Closed

Python: Add MCP-based skills support (skill-md type)#6149
semenshi-m (semenshi) wants to merge 3 commits into
microsoft:mainfrom
semenshi:python/add-mcp-skills-source

Conversation

@semenshi

Copy link
Copy Markdown
Contributor

Description

Port of .NET PR #6108 to Python. Adds MCP-based skills support (skill-md type) following the SEP-2640 convention.

New classes

  • McpSkillsSource - discovers Agent Skills from MCP servers by reading skill://index.json
  • McpSkill - lazy-loads SKILL.md content via MCP resources/read, resolves sibling resources on demand
  • McpSkillResource - wraps pre-fetched ReadResourceResult (text or binary)

Design decisions

  • Classes added to _skills.py alongside existing skill types since mcp>=1.24.0 is already a core dependency
  • Only skill-md entries supported; archive and mcp-resource-template silently skipped
  • Content caching uses asyncio.Lock for thread-safe lazy loading

Validation

  • 35 new unit tests (mock-based)
  • All existing skills tests pass
  • Ruff clean, no new pyright errors

Motivation and Context

Enables Python agents to discover and load skills from MCP servers, matching .NET PR #6108.

…okup

Port of .NET commit 08541ee.

Replace property-based Skill.content/resources/scripts with async
by-name lookup methods:
- content property -> async get_content() -> str
- resources property -> async get_resource(name) -> SkillResource | None
- scripts property -> async get_script(name) -> SkillScript | None

SkillsProvider now always includes all three tools (load_skill,
read_skill_resource, run_skill_script) and both instruction blocks
regardless of whether any skills have resources or scripts.

ClassSkill retains resources/scripts properties as overridable hooks
for subclass reflection-based discovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 28, 2026 13:02
@moonbox3

Evan Mattson (moonbox3) commented May 28, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _skills.py10043996%295, 542, 1008, 1023, 1025–1026, 1382–1383, 1395–1396, 1626, 1655, 2118, 2577–2578, 2713, 2718, 2721, 2726, 2753, 2758, 2812, 2821, 2826, 2829, 2834, 2858, 2863, 3105–3106, 3509–3510, 3611, 3616–3617, 3644–3645, 3652–3653
TOTAL36995433988% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7423 34 💤 0 ❌ 0 🔥 1m 55s ⏱️

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.

Pull request overview

Adds MCP-backed skill discovery/loading to the Python Agent Framework (porting the .NET “skill-md” MCP skills work), and updates the core skills plumbing to support async/lazy content + on-demand resource reads.

Changes:

  • Introduces McpSkillsSource, McpSkill, and McpSkillResource to discover skills from skill://index.json and lazily load SKILL.md via resources/read.
  • Refactors Skill to use async get_content() plus async lookup methods (get_resource(), get_script()), and updates SkillsProvider tool wiring accordingly.
  • Adds a new MCP-skills unit test suite and updates existing skills tests for the async API/tool behavior changes.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
python/packages/core/agent_framework/_skills.py Core implementation changes: async skill content API + new MCP skills source/skill/resource types + provider tool wiring updates.
python/packages/core/agent_framework/init.py Exposes MCP skill types as public exports.
python/packages/core/tests/core/test_skills.py Updates existing skills tests for async content loading and updated tool set.
python/packages/core/tests/core/test_mcp_skills.py Adds coverage for MCP index parsing, MCP skill lazy content loading, and MCP resource reading.

Comment on lines +3497 to +3501
if not name or not name.strip():
return None

uri = self._skill_root_uri + name
try:
Comment on lines +3501 to +3506
try:
result = await self._client.read_resource(_mcp_any_url(uri))
except Exception as ex:
from mcp.shared.exceptions import McpError

if isinstance(ex, McpError):
Comment on lines +3602 to +3611
try:
result = await self._client.read_resource(_mcp_any_url(self._INDEX_URI))
except Exception as ex:
from mcp.shared.exceptions import McpError

if isinstance(ex, McpError):
logger.debug("No skill://index.json resource available on MCP server: %s", ex)
else:
logger.warning("Failed to read skill://index.json from MCP server.", exc_info=True)
return None
Comment on lines 2025 to 2029
instructions = self._create_instructions(
prompt_template=self._instruction_template,
skills=skills,
include_script_runner_instructions=has_scripts,
include_resource_instructions=has_resources,
)

Comment on lines 830 to 831
def resource(
self,
Comment on lines +190 to +194
# Binary content from BlobResourceContents (second item) should NOT take precedence
# since the first item is text. The loop finds text first, but the implementation
# checks BlobResourceContents first in the iteration order.
# Actually: the implementation iterates all contents looking for BlobResourceContents first.
# So the text item (first) is not a blob, the blob item (second) IS a blob -> returns bytes.

@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: 2 | Confidence: 81%

✓ Correctness

No actionable issues found in this dimension.

✓ Test Coverage

The test suite for the new MCP skills support is generally comprehensive with 35 tests covering core functionality. However, the path traversal test (test_get_resource_path_traversal_returns_none) provides a false sense of security: it passes only because the mock client lacks the traversal URI, not because the implementation actually validates against path traversal. The production McpSkill.get_resource simply concatenates the root URI with the user-provided name and makes an MCP request — no traversal check exists. Additionally, there's no test for McpSkill.get_content propagating non-McpError exceptions (e.g., network failures). Test coverage is thorough with meaningful assertions for caching, error cases, tool registration, and script discovery. One minor naming issue: test_custom_template_without_runner_placeholder_raises no longer tests that it raises, but the docstring was updated to reflect the new behavior.


Automated review by semenshi's agents

assert await skill.get_resource("") is None
assert await skill.get_resource(" ") is None

@pytest.mark.asyncio

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.

This test gives a false sense of security: get_resource returns None here only because the mock raises McpError for unknown URIs—not because the implementation validates against path traversal. To properly test traversal protection (once the implementation-side fix from the existing review comment is applied), assert that client.read_resource is never called when a traversal name is provided, confirming the name is rejected before any I/O.

Suggested change
@pytest.mark.asyncio
@pytest.mark.asyncio
@pytest.mark.parametrize("name", ["../escape.md", "references/../../escape.md", "."])
async def test_get_resource_path_traversal_returns_none(self, name: str) -> None:
client = _make_client(**{"skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD)})
from agent_framework import SkillFrontmatter
fm = SkillFrontmatter(name="unit-converter", description="Convert between common units.")
skill = McpSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client)
resource = await skill.get_resource(name)
assert resource is None
# Verify traversal is rejected before making any MCP call
client.read_resource.assert_not_called()

semenshi-m (semenshi) and others added 2 commits May 28, 2026 15:49
Adds McpSkill, McpSkillResource, and McpSkillsSource so agents can discover and load skills served over the Model Context Protocol following the SEP-2640 convention (skill://index.json + per-skill SKILL.md resources).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Demonstrates discovering Agent Skills served over MCP using McpSkillsSource by connecting to a remote MCP server (configured via MCP_SKILLS_SERVER_URL) that exposes skill resources following the SEP-2640 convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@semenshi
semenshi-m (semenshi) force-pushed the python/add-mcp-skills-source branch from 8e2513f to 8b8da86 Compare May 28, 2026 14:50
@moonbox3 Evan Mattson (moonbox3) added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels May 28, 2026
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.

3 participants