From 6e8f7aa03fda26b19c6f10405abf32ca769b53d0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Fri, 29 May 2026 11:23:59 +0100 Subject: [PATCH] Address PR review comments: rename to MCP* convention, fix error handling and samples - Rename McpSkill/McpSkillResource/McpSkillsSource to MCPSkill/MCPSkillResource/MCPSkillsSource - Add data-URI prefix stripping for blob resource decoding - Let non-McpError exceptions propagate from get_resource() - Fix contradictory test comment - Use interactive input() in mcp_based_skill sample - Remove misleading sample output block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../packages/core/agent_framework/__init__.py | 12 +-- .../packages/core/agent_framework/_skills.py | 54 ++++++------ .../core/tests/core/test_mcp_skills.py | 85 +++++++++---------- .../02-agents/providers/foundry/README.md | 2 +- ...foundry_chat_client_with_toolbox_skills.py | 8 +- python/samples/02-agents/skills/README.md | 2 +- .../skills/mcp_based_skill/README.md | 2 +- .../skills/mcp_based_skill/mcp_based_skill.py | 30 ++----- 8 files changed, 92 insertions(+), 103 deletions(-) diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index 589e71fb60..2ddfe25ea6 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -157,9 +157,9 @@ from ._skills import ( InlineSkillResource, InlineSkillScript, InMemorySkillsSource, - McpSkill, - McpSkillResource, - McpSkillsSource, + MCPSkill, + MCPSkillResource, + MCPSkillsSource, Skill, SkillFrontmatter, SkillResource, @@ -428,9 +428,9 @@ __all__ = [ "MCPStdioTool", "MCPStreamableHTTPTool", "MCPWebsocketTool", - "McpSkill", - "McpSkillResource", - "McpSkillsSource", + "MCPSkill", + "MCPSkillResource", + "MCPSkillsSource", "MemoryContextProvider", "MemoryFileStore", "MemoryIndexEntry", diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index c77c5dca1c..e4f0cbc98e 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -3390,16 +3390,16 @@ def _parse_mcp_skill_index(text: str) -> _McpSkillIndex: @experimental(feature_id=ExperimentalFeature.SKILLS) -class McpSkillResource(SkillResource): +class MCPSkillResource(SkillResource): """A :class:`SkillResource` backed by content fetched from an MCP server. The :class:`~mcp.types.ReadResourceResult` is fetched eagerly by - :meth:`McpSkill.get_resource` at construction time; :meth:`read` + :meth:`MCPSkill.get_resource` at construction time; :meth:`read` extracts text or binary content from the result. """ def __init__(self, *, name: str, result: ReadResourceResult) -> None: - """Initialize an McpSkillResource. + """Initialize an MCPSkillResource. Args: name: The resource name (e.g. a relative path or identifier). @@ -3420,14 +3420,19 @@ class McpSkillResource(SkillResource): for content in self._result.contents: if isinstance(content, BlobResourceContents): - return base64.b64decode(content.blob) + blob = content.blob + # Strip data-URI prefix if present (some MCP servers send + # full data URIs instead of raw base64). + if blob.startswith("data:"): + blob = blob.split(",", 1)[-1] + return base64.b64decode(blob) text = "\n".join(c.text for c in self._result.contents if isinstance(c, TextResourceContents)) return text if text else None @experimental(feature_id=ExperimentalFeature.SKILLS) -class McpSkill(Skill): +class MCPSkill(Skill): """A :class:`Skill` discovered from an MCP server exposing the Agent Skills convention. The skill is constructed from ``skill://index.json`` discovery metadata; @@ -3435,9 +3440,9 @@ class McpSkill(Skill): server on demand via ``resources/read``. Per SEP-2640, resources referenced inside SKILL.md are fetched on demand - via the originating MCP server: :meth:`get_resource` resolves a relative + via the originating MCP server: :meth:`get_resource` resolves a relative resource name against the skill's root URI, issues a ``resources/read`` - request, and returns an :class:`McpSkillResource` with pre-fetched content. + request, and returns an :class:`MCPSkillResource` with pre-fetched content. """ _SKILL_MD_SUFFIX: Final[str] = "SKILL.md" @@ -3448,7 +3453,7 @@ class McpSkill(Skill): skill_md_uri: str, client: ClientSession, ) -> None: - """Initialize an McpSkill. + """Initialize an MCPSkill. Args: frontmatter: The parsed frontmatter metadata for this skill. @@ -3498,13 +3503,13 @@ class McpSkill(Skill): Resolves *name* as a relative path against the skill's root URI, issues a ``resources/read`` request to the MCP server, and returns - an :class:`McpSkillResource` with the pre-fetched content. + an :class:`MCPSkillResource` with the pre-fetched content. Args: name: The resource name (e.g. ``references/checklist.md``). Returns: - An :class:`McpSkillResource`, or ``None`` when the name is empty + An :class:`MCPSkillResource`, or ``None`` when the name is empty or the resource does not exist on the server. """ if not name or not name.strip(): @@ -3522,10 +3527,9 @@ class McpSkill(Skill): if isinstance(ex, McpError): return None - logger.debug("Unexpected error reading MCP resource '%s'", uri, exc_info=True) - return None + raise - return McpSkillResource(name=name, result=result) + return MCPSkillResource(name=name, result=result) @staticmethod def _validate_resource_name(name: str) -> str | None: @@ -3561,20 +3565,20 @@ class McpSkill(Skill): If the URI doesn't end with ``SKILL.md``, ensures it ends with a trailing slash. """ - if skill_md_uri.endswith(McpSkill._SKILL_MD_SUFFIX): - return skill_md_uri[: -len(McpSkill._SKILL_MD_SUFFIX)] + if skill_md_uri.endswith(MCPSkill._SKILL_MD_SUFFIX): + return skill_md_uri[: -len(MCPSkill._SKILL_MD_SUFFIX)] if skill_md_uri.endswith("/"): return skill_md_uri return skill_md_uri + "/" @experimental(feature_id=ExperimentalFeature.SKILLS) -class McpSkillsSource(SkillsSource): +class MCPSkillsSource(SkillsSource): """A :class:`SkillsSource` that discovers Agent Skills served over MCP. Discovery follows the SEP-2640 recommended approach: the source reads the well-known ``skill://index.json`` resource and constructs one - :class:`McpSkill` per ``skill-md`` entry directly from the entry's + :class:`MCPSkill` per ``skill-md`` entry directly from the entry's ``name``, ``description``, and ``url`` fields. The referenced ``SKILL.md`` resource is **not** read during discovery; @@ -3592,7 +3596,7 @@ class McpSkillsSource(SkillsSource): from mcp.client.session import ClientSession - source = McpSkillsSource(client=session) + source = MCPSkillsSource(client=session) skills = await source.get_skills() """ @@ -3600,7 +3604,7 @@ class McpSkillsSource(SkillsSource): _SKILL_MD_TYPE: Final[str] = "skill-md" def __init__(self, client: ClientSession) -> None: - """Initialize an McpSkillsSource. + """Initialize an MCPSkillsSource. Args: client: An MCP client session connected to a server that @@ -3612,10 +3616,10 @@ class McpSkillsSource(SkillsSource): """Discover and return skills from the MCP server. Reads ``skill://index.json``, parses it, and creates an - :class:`McpSkill` for each valid ``skill-md`` entry. + :class:`MCPSkill` for each valid ``skill-md`` entry. Returns: - A list of discovered :class:`McpSkill` instances. + A list of discovered :class:`MCPSkill` instances. """ index = await self._try_read_index() if index is None: @@ -3665,14 +3669,14 @@ class McpSkillsSource(SkillsSource): logger.warning("Failed to parse skill://index.json JSON document.", exc_info=True) return None - def _try_create_skill(self, entry: _McpSkillIndexEntry) -> McpSkill | None: - """Attempt to create an :class:`McpSkill` from an index entry. + def _try_create_skill(self, entry: _McpSkillIndexEntry) -> MCPSkill | None: + """Attempt to create an :class:`MCPSkill` from an index entry. Args: entry: A single entry from the skill index. Returns: - An :class:`McpSkill` if the entry is valid, or ``None`` if the + An :class:`MCPSkill` if the entry is valid, or ``None`` if the entry should be skipped. """ if entry.type != self._SKILL_MD_TYPE: @@ -3701,7 +3705,7 @@ class McpSkillsSource(SkillsSource): logger.debug("Skipping entry '%s': invalid metadata: %s", entry.name, ex) return None - return McpSkill(frontmatter=fm, skill_md_uri=entry.url, client=self._client) + return MCPSkill(frontmatter=fm, skill_md_uri=entry.url, client=self._client) # endregion diff --git a/python/packages/core/tests/core/test_mcp_skills.py b/python/packages/core/tests/core/test_mcp_skills.py index 814abda850..853e20a28d 100644 --- a/python/packages/core/tests/core/test_mcp_skills.py +++ b/python/packages/core/tests/core/test_mcp_skills.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -"""Tests for MCP-based skills (McpSkillsSource, McpSkill, McpSkillResource).""" +"""Tests for MCP-based skills (MCPSkillsSource, MCPSkill, MCPSkillResource).""" from __future__ import annotations @@ -18,7 +18,7 @@ from mcp.types import ( ) from pydantic import AnyUrl -from agent_framework import McpSkill, McpSkillResource, McpSkillsSource +from agent_framework import MCPSkill, MCPSkillResource, MCPSkillsSource from agent_framework._skills import _parse_mcp_skill_index # --------------------------------------------------------------------------- @@ -97,7 +97,7 @@ def _make_client(**read_resource_responses: ReadResourceResult) -> AsyncMock: # --------------------------------------------------------------------------- -class TestParseMcpSkillIndex: +class TestParseMCPSkillIndex: """Tests for the _parse_mcp_skill_index helper.""" def test_parses_valid_index(self) -> None: @@ -131,17 +131,17 @@ class TestParseMcpSkillIndex: # --------------------------------------------------------------------------- -# McpSkillResource tests +# MCPSkillResource tests # --------------------------------------------------------------------------- -class TestMcpSkillResource: - """Tests for McpSkillResource.""" +class TestMCPSkillResource: + """Tests for MCPSkillResource.""" @pytest.mark.asyncio async def test_read_text_content(self) -> None: result = _make_text_result("hello world") - resource = McpSkillResource(name="test.md", result=result) + resource = MCPSkillResource(name="test.md", result=result) content = await resource.read() assert content == "hello world" @@ -149,14 +149,14 @@ class TestMcpSkillResource: async def test_read_binary_content(self) -> None: data = bytes([0x01, 0x02, 0x03, 0x04]) result = _make_blob_result(data) - resource = McpSkillResource(name="icon.bin", result=result) + resource = MCPSkillResource(name="icon.bin", result=result) content = await resource.read() assert content == data @pytest.mark.asyncio async def test_read_empty_returns_none(self) -> None: result = _make_empty_result() - resource = McpSkillResource(name="empty", result=result) + resource = MCPSkillResource(name="empty", result=result) content = await resource.read() assert content is None @@ -168,7 +168,7 @@ class TestMcpSkillResource: TextResourceContents(uri=AnyUrl("skill://b"), text="line2", mimeType="text/plain"), ] ) - resource = McpSkillResource(name="multi", result=result) + resource = MCPSkillResource(name="multi", result=result) content = await resource.read() assert content == "line1\nline2" @@ -185,23 +185,20 @@ class TestMcpSkillResource: ), ] ) - resource = McpSkillResource(name="mixed", result=result) + resource = MCPSkillResource(name="mixed", result=result) content = await resource.read() - # 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. + # The implementation iterates all contents checking for BlobResourceContents + # first, so when both text and binary are present, binary is returned. assert content == data # --------------------------------------------------------------------------- -# McpSkill tests +# MCPSkill tests # --------------------------------------------------------------------------- -class TestMcpSkill: - """Tests for McpSkill.""" +class TestMCPSkill: + """Tests for MCPSkill.""" @pytest.mark.asyncio async def test_get_content_fetches_and_caches(self) -> None: @@ -209,7 +206,7 @@ class TestMcpSkill: 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) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) content1 = await skill.get_content() content2 = await skill.get_content() @@ -225,7 +222,7 @@ class TestMcpSkill: from agent_framework import SkillFrontmatter fm = SkillFrontmatter(name="empty-skill", description="Empty skill.") - skill = McpSkill(frontmatter=fm, skill_md_uri="skill://empty/SKILL.md", client=client) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://empty/SKILL.md", client=client) with pytest.raises(ValueError, match="no text content"): await skill.get_content() @@ -241,7 +238,7 @@ class TestMcpSkill: 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) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) resource = await skill.get_resource("references/checklist.md") assert resource is not None @@ -260,7 +257,7 @@ class TestMcpSkill: 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) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) resource = await skill.get_resource("assets/icon.bin") assert resource is not None @@ -273,7 +270,7 @@ class TestMcpSkill: 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) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) resource = await skill.get_resource("references/does-not-exist.md") assert resource is None @@ -300,7 +297,7 @@ class TestMcpSkill: 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) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://unit-converter/SKILL.md", client=client) resource = await skill.get_resource(name) assert resource is None @@ -312,7 +309,7 @@ class TestMcpSkill: from agent_framework import SkillFrontmatter fm = SkillFrontmatter(name="test-skill", description="Test.") - skill = McpSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) assert await skill.get_resource("") is None assert await skill.get_resource(" ") is None @@ -323,27 +320,27 @@ class TestMcpSkill: from agent_framework import SkillFrontmatter fm = SkillFrontmatter(name="test-skill", description="Test.") - skill = McpSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) + skill = MCPSkill(frontmatter=fm, skill_md_uri="skill://test/SKILL.md", client=client) assert await skill.get_script("anything") is None def test_compute_skill_root_uri_strips_suffix(self) -> None: - assert McpSkill._compute_skill_root_uri("skill://unit-converter/SKILL.md") == "skill://unit-converter/" + assert MCPSkill._compute_skill_root_uri("skill://unit-converter/SKILL.md") == "skill://unit-converter/" def test_compute_skill_root_uri_trailing_slash(self) -> None: - assert McpSkill._compute_skill_root_uri("skill://unit-converter/") == "skill://unit-converter/" + assert MCPSkill._compute_skill_root_uri("skill://unit-converter/") == "skill://unit-converter/" def test_compute_skill_root_uri_no_suffix_adds_slash(self) -> None: - assert McpSkill._compute_skill_root_uri("skill://unit-converter") == "skill://unit-converter/" + assert MCPSkill._compute_skill_root_uri("skill://unit-converter") == "skill://unit-converter/" # --------------------------------------------------------------------------- -# McpSkillsSource tests +# MCPSkillsSource tests # --------------------------------------------------------------------------- -class TestMcpSkillsSource: - """Tests for McpSkillsSource.""" +class TestMCPSkillsSource: + """Tests for MCPSkillsSource.""" @pytest.mark.asyncio async def test_index_based_discovery_returns_skill(self) -> None: @@ -353,7 +350,7 @@ class TestMcpSkillsSource: "skill://unit-converter/SKILL.md": _make_text_result(SAMPLE_SKILL_MD), } ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert len(skills) == 1 @@ -367,7 +364,7 @@ class TestMcpSkillsSource: @pytest.mark.asyncio async def test_no_index_returns_empty(self) -> None: client = _make_client() # No resources at all - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -378,7 +375,7 @@ class TestMcpSkillsSource: client = _make_client( **{"skill://index.json": _make_text_result(SAMPLE_SKILL_INDEX, uri="skill://index.json")} ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert len(skills) == 1 @@ -400,7 +397,7 @@ class TestMcpSkillsSource: } ) client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -419,7 +416,7 @@ class TestMcpSkillsSource: } ) client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -439,7 +436,7 @@ class TestMcpSkillsSource: } ) client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -458,7 +455,7 @@ class TestMcpSkillsSource: } ) client = _make_client(**{"skill://index.json": _make_text_result(index_json, uri="skill://index.json")}) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -467,7 +464,7 @@ class TestMcpSkillsSource: client = _make_client( **{"skill://index.json": _make_text_result('{"skills": []}', uri="skill://index.json")} ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -476,7 +473,7 @@ class TestMcpSkillsSource: client = _make_client( **{"skill://index.json": _make_text_result("not valid json", uri="skill://index.json")} ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skills = await source.get_skills() assert skills == [] @@ -489,7 +486,7 @@ class TestMcpSkillsSource: "skill://unit-converter/references/checklist.md": _make_text_result("- check thing 1\n- check thing 2"), } ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skill = (await source.get_skills())[0] resource = await skill.get_resource("references/checklist.md") assert resource is not None @@ -506,7 +503,7 @@ class TestMcpSkillsSource: "skill://unit-converter/assets/icon.bin": _make_blob_result(data), } ) - source = McpSkillsSource(client=client) + source = MCPSkillsSource(client=client) skill = (await source.get_skills())[0] resource = await skill.get_resource("assets/icon.bin") assert resource is not None diff --git a/python/samples/02-agents/providers/foundry/README.md b/python/samples/02-agents/providers/foundry/README.md index 1d4758b0b4..1a3c06ffda 100644 --- a/python/samples/02-agents/providers/foundry/README.md +++ b/python/samples/02-agents/providers/foundry/README.md @@ -27,7 +27,7 @@ This folder contains Azure AI Foundry and Foundry Local samples for Agent Framew | [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | Foundry Chat Client with local MCP | | [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Foundry Chat Client with session management | | [`foundry_chat_client_with_toolbox.py`](foundry_chat_client_with_toolbox.py) | Foundry Chat Client connected to a toolbox via its MCP endpoint using `MCPStreamableHTTPTool` | -| [`foundry_chat_client_with_toolbox_skills.py`](foundry_chat_client_with_toolbox_skills.py) | Foundry Chat Client that discovers MCP-based skills from a Foundry Toolbox endpoint via `McpSkillsSource` (uses an Azure AD bearer token and the toolbox preview header) | +| [`foundry_chat_client_with_toolbox_skills.py`](foundry_chat_client_with_toolbox_skills.py) | Foundry Chat Client that discovers MCP-based skills from a Foundry Toolbox endpoint via `MCPSkillsSource` (uses an Azure AD bearer token and the toolbox preview header) | ## FoundryLocalClient Samples diff --git a/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py index 559ca76ab2..4e41460341 100644 --- a/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py +++ b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox_skills.py @@ -5,7 +5,7 @@ import os from collections.abc import Generator import httpx -from agent_framework import Agent, McpSkillsSource, SkillsProvider +from agent_framework import Agent, MCPSkillsSource, SkillsProvider from agent_framework.foundry import FoundryChatClient from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider @@ -20,7 +20,7 @@ load_dotenv() Foundry Chat Client with Toolbox-Hosted Skills Discover Agent Skills served by a Microsoft Foundry Toolbox MCP endpoint -and inject them into a ``FoundryChatClient`` agent via ``McpSkillsSource``. +and inject them into a ``FoundryChatClient`` agent via ``MCPSkillsSource``. The toolbox's discovery document (``skill://index.json``) is read once at startup; SKILL.md bodies are fetched on demand as the agent uses them. @@ -72,11 +72,11 @@ async def main() -> None: await session.initialize() # Discover skills served by the toolbox and inject them as a context provider. - skills_provider = SkillsProvider(McpSkillsSource(client=session)) + skills_provider = SkillsProvider(MCPSkillsSource(client=session)) async with Agent( client=FoundryChatClient(credential=credential), - name="ToolboxMcpSkillsAgent", + name="ToolboxMCPSkillsAgent", instructions="You are a helpful assistant. Use available skills to answer the user.", context_providers=[skills_provider], ) as agent: diff --git a/python/samples/02-agents/skills/README.md b/python/samples/02-agents/skills/README.md index 9236ebc1bd..53a2167531 100644 --- a/python/samples/02-agents/skills/README.md +++ b/python/samples/02-agents/skills/README.md @@ -12,7 +12,7 @@ Start with file-based or code-defined skills, then explore combining them and ad | [**code_defined_skill**](code_defined_skill/) | Define skills entirely in Python code using `Skill`, `@skill.resource`, and `@skill.script` decorators. Uses a code-defined unit-converter skill. | | [**class_based_skill**](class_based_skill/) | Define skills as Python classes using `ClassSkill` with `@ClassSkill.resource` and `@ClassSkill.script` decorators for auto-discovery. Uses a class-based unit-converter skill. | | [**mixed_skills**](mixed_skills/) | Combine code-defined, class-based, and file-based skills in a single agent. Uses a code-defined volume-converter, a class-based temperature-converter, and a file-based unit-converter. | -| [**mcp_based_skill**](mcp_based_skill/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `McpSkillsSource`. Connects to a remote MCP server that exposes skills as `skill://...` resources following the SEP-2640 convention. | +| [**mcp_based_skill**](mcp_based_skill/) | Discover skills served over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) via `MCPSkillsSource`. Connects to a remote MCP server that exposes skills as `skill://...` resources following the SEP-2640 convention. | | [**script_approval**](script_approval/) | Require human-in-the-loop approval before executing skill scripts | ## Key Concepts diff --git a/python/samples/02-agents/skills/mcp_based_skill/README.md b/python/samples/02-agents/skills/mcp_based_skill/README.md index a4daf32efe..994fa2d7fb 100644 --- a/python/samples/02-agents/skills/mcp_based_skill/README.md +++ b/python/samples/02-agents/skills/mcp_based_skill/README.md @@ -6,7 +6,7 @@ This sample demonstrates how to discover **Agent Skills served over MCP** with a - Connecting to a remote MCP server (over streamable HTTP) that exposes skill resources following the SEP-2640 convention. -- Building a `SkillsProvider` from an `McpSkillsSource`, which reads +- Building a `SkillsProvider` from an `MCPSkillsSource`, which reads `skill://index.json` (SEP-2640 canonical discovery) and constructs skills from the index entries. - The progressive disclosure pattern across MCP: advertise → load → read diff --git a/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py b/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py index 6ebcf31e4c..348f972203 100644 --- a/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py +++ b/python/samples/02-agents/skills/mcp_based_skill/mcp_based_skill.py @@ -7,7 +7,7 @@ import os # using the sample's Skills APIs. # import warnings # warnings.filterwarnings("ignore", message=r"\[SKILLS\].*", category=FutureWarning) -from agent_framework import Agent, McpSkillsSource, SkillsProvider +from agent_framework import Agent, MCPSkillsSource, SkillsProvider from agent_framework.foundry import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv @@ -18,7 +18,7 @@ from mcp.client.streamable_http import streamable_http_client MCP-Based Agent Skills This sample demonstrates how to discover Agent Skills served over the -Model Context Protocol (MCP) using :class:`McpSkillsSource`. +Model Context Protocol (MCP) using :class:`MCPSkillsSource`. The sample connects to a remote MCP server that exposes skill resources under the ``skill://`` URI scheme: @@ -47,10 +47,10 @@ async def main() -> None: await session.initialize() # 2. Build a SkillsProvider that discovers skills over MCP. - # McpSkillsSource reads skill://index.json and creates one - # McpSkill per skill-md entry; SKILL.md bodies are fetched + # MCPSkillsSource reads skill://index.json and creates one + # MCPSkill per skill-md entry; SKILL.md bodies are fetched # on demand via resources/read. - skills_provider = SkillsProvider(McpSkillsSource(client=session)) + skills_provider = SkillsProvider(MCPSkillsSource(client=session)) # 3. Run the agent. client = FoundryChatClient( @@ -64,25 +64,13 @@ async def main() -> None: instructions="You are a helpful assistant. Use available skills to answer the user.", context_providers=[skills_provider], ) as agent: - response = await agent.run( - "What skills do you have?" - ) + query = input("User: ").strip() # noqa: ASYNC250 + if not query: + return + response = await agent.run(query) print(f"Agent: {response}\n") if __name__ == "__main__": asyncio.run(main()) - -""" -Sample output: - -Discovering MCP-based skills ------------------------------------------------------------- -Agent: Here are your conversions: - -1. **26.2 miles -> 42.16 km** (a marathon distance) -2. **75 kg -> 165.35 lbs** - -Conversion factors used: miles * 1.60934 and kilograms * 2.20462. -"""