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."""