Skip to content

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

Closed
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 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
Copy link
Copy Markdown
Contributor

moonbox3 commented May 28, 2026

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 ⏱️

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

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

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 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 force-pushed the python/add-mcp-skills-source branch from 8e2513f to 8b8da86 Compare May 28, 2026 14:50
@moonbox3 moonbox3 added documentation Improvements or additions to documentation python labels May 28, 2026
@semenshi semenshi closed this May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants