From aa7946d353e743f81d31d14378cd7cddd7867b35 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:24:39 -0500 Subject: [PATCH] Preserve skill metadata through provider wrapping (#3237) * Preserve computed _meta through provider wrapping; add skill identity to _meta * chore: Update SDK documentation --------- Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> --- ...server-providers-skills-skill_provider.mdx | 28 +++++++--- .../server/providers/fastmcp_provider.py | 8 +-- .../server/providers/skills/skill_provider.py | 19 ++++++- .../server/providers/test_skills_provider.py | 53 +++++++++++++++++++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx b/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx index 3d3b19515..c1ab56d9d 100644 --- a/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx +++ b/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx @@ -18,7 +18,13 @@ A resource representing a skill's main file or manifest. **Methods:** -#### `read` +#### `get_meta` + +```python +get_meta(self) -> dict[str, Any] +``` + +#### `read` ```python read(self) -> str | bytes | ResourceResult @@ -27,7 +33,7 @@ read(self) -> str | bytes | ResourceResult Read the resource content. -### `SkillFileTemplate` +### `SkillFileTemplate` A template for accessing files within a skill. @@ -35,7 +41,7 @@ A template for accessing files within a skill. **Methods:** -#### `read` +#### `read` ```python read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult @@ -44,7 +50,7 @@ read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult Read a file from the skill directory. -#### `create_resource` +#### `create_resource` ```python create_resource(self, uri: str, params: dict[str, Any]) -> Resource @@ -56,7 +62,7 @@ Note: This is not typically used since _read() handles file reading directly. Provided for compatibility with the ResourceTemplate interface. -### `SkillFileResource` +### `SkillFileResource` A resource representing a specific file within a skill. @@ -64,7 +70,13 @@ A resource representing a specific file within a skill. **Methods:** -#### `read` +#### `get_meta` + +```python +get_meta(self) -> dict[str, Any] +``` + +#### `read` ```python read(self) -> str | bytes | ResourceResult @@ -73,7 +85,7 @@ read(self) -> str | bytes | ResourceResult Read the file content. -### `SkillProvider` +### `SkillProvider` Provider that exposes a single skill folder as MCP resources. @@ -99,7 +111,7 @@ manifest) are exposed to clients\: **Methods:** -#### `skill_info` +#### `skill_info` ```python skill_info(self) -> SkillInfo diff --git a/src/fastmcp/server/providers/fastmcp_provider.py b/src/fastmcp/server/providers/fastmcp_provider.py index 82476ffb9..cab6bc541 100644 --- a/src/fastmcp/server/providers/fastmcp_provider.py +++ b/src/fastmcp/server/providers/fastmcp_provider.py @@ -86,7 +86,7 @@ class FastMCPProviderTool(Tool): tags=tool.tags, annotations=tool.annotations, task_config=tool.task_config, - meta=tool.meta, + meta=tool.get_meta(), title=tool.title, icons=tool.icons, ) @@ -186,7 +186,7 @@ class FastMCPProviderResource(Resource): tags=resource.tags, annotations=resource.annotations, task_config=resource.task_config, - meta=resource.meta, + meta=resource.get_meta(), title=resource.title, icons=resource.icons, ) @@ -255,7 +255,7 @@ class FastMCPProviderPrompt(Prompt): arguments=prompt.arguments, tags=prompt.tags, task_config=prompt.task_config, - meta=prompt.meta, + meta=prompt.get_meta(), title=prompt.title, icons=prompt.icons, ) @@ -359,7 +359,7 @@ class FastMCPProviderResourceTemplate(ResourceTemplate): tags=template.tags, annotations=template.annotations, task_config=template.task_config, - meta=template.meta, + meta=template.get_meta(), title=template.title, icons=template.icons, ) diff --git a/src/fastmcp/server/providers/skills/skill_provider.py b/src/fastmcp/server/providers/skills/skill_provider.py index 5b8392e42..86bcba5e3 100644 --- a/src/fastmcp/server/providers/skills/skill_provider.py +++ b/src/fastmcp/server/providers/skills/skill_provider.py @@ -6,7 +6,7 @@ import json import mimetypes from collections.abc import Sequence from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, cast from pydantic import AnyUrl @@ -38,6 +38,15 @@ class SkillResource(Resource): skill_info: SkillInfo is_manifest: bool = False + def get_meta(self) -> dict[str, Any]: + meta = super().get_meta() + fastmcp = cast(dict[str, Any], meta["fastmcp"]) + fastmcp["skill"] = { + "name": self.skill_info.name, + "is_manifest": self.is_manifest, + } + return meta + async def read(self) -> str | bytes | ResourceResult: """Read the resource content.""" if self.is_manifest: @@ -135,6 +144,14 @@ class SkillFileResource(Resource): skill_info: SkillInfo file_path: str + def get_meta(self) -> dict[str, Any]: + meta = super().get_meta() + fastmcp = cast(dict[str, Any], meta["fastmcp"]) + fastmcp["skill"] = { + "name": self.skill_info.name, + } + return meta + async def read(self) -> str | bytes | ResourceResult: """Read the file content.""" full_path = self.skill_info.path / self.file_path diff --git a/tests/server/providers/test_skills_provider.py b/tests/server/providers/test_skills_provider.py index 36c527308..40ec0fac7 100644 --- a/tests/server/providers/test_skills_provider.py +++ b/tests/server/providers/test_skills_provider.py @@ -187,6 +187,59 @@ This is my skill content. result = await client.read_resource(AnyUrl("skill://my-skill/reference.md")) assert "# Reference" in result[0].text + async def test_skill_resource_meta(self, single_skill_dir: Path): + """SkillResource populates meta with skill name and is_manifest.""" + provider = SkillProvider(skill_path=single_skill_dir) + resources = await provider.list_resources() + + by_name = {r.name: r for r in resources} + + main_meta = by_name["my-skill/SKILL.md"].get_meta() + assert main_meta["fastmcp"]["skill"] == { + "name": "my-skill", + "is_manifest": False, + } + + manifest_meta = by_name["my-skill/_manifest"].get_meta() + assert manifest_meta["fastmcp"]["skill"] == { + "name": "my-skill", + "is_manifest": True, + } + + async def test_skill_file_resource_meta(self, single_skill_dir: Path): + """SkillFileResource populates meta with skill name.""" + provider = SkillProvider( + skill_path=single_skill_dir, supporting_files="resources" + ) + resources = await provider.list_resources() + + by_name = {r.name: r for r in resources} + file_meta = by_name["my-skill/reference.md"].get_meta() + assert file_meta["fastmcp"]["skill"] == {"name": "my-skill"} + + async def test_skill_meta_survives_mounting(self, single_skill_dir: Path): + """Skill metadata in _meta is preserved when accessed through a mounted server.""" + child = FastMCP("child") + child.add_provider(SkillProvider(skill_path=single_skill_dir)) + + parent = FastMCP("parent") + parent.mount(child, "skills") + + resources = await parent.list_resources() + by_name = {r.name: r for r in resources} + + main_meta = by_name["my-skill/SKILL.md"].get_meta() + assert main_meta["fastmcp"]["skill"] == { + "name": "my-skill", + "is_manifest": False, + } + + manifest_meta = by_name["my-skill/_manifest"].get_meta() + assert manifest_meta["fastmcp"]["skill"] == { + "name": "my-skill", + "is_manifest": True, + } + class TestSkillsDirectoryProvider: """Tests for SkillsDirectoryProvider - scans directory for skill folders."""