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>
This commit is contained in:
Jeremiah Lowin 2026-02-19 12:24:39 -05:00 committed by GitHub
commit aa7946d353
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 13 deletions

View file

@ -18,7 +18,13 @@ A resource representing a skill's main file or manifest.
**Methods:**
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_meta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_meta(self) -> dict[str, Any]
```
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> str | bytes | ResourceResult
@ -27,7 +33,7 @@ read(self) -> str | bytes | ResourceResult
Read the resource content.
### `SkillFileTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SkillFileTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A template for accessing files within a skill.
@ -35,7 +41,7 @@ A template for accessing files within a skill.
**Methods:**
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L115" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```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` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SkillFileResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A resource representing a specific file within a skill.
@ -64,7 +70,13 @@ A resource representing a specific file within a skill.
**Methods:**
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L138" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_meta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_meta(self) -> dict[str, Any]
```
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L155" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> str | bytes | ResourceResult
@ -73,7 +85,7 @@ read(self) -> str | bytes | ResourceResult
Read the file content.
### `SkillProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L162" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SkillProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Provider that exposes a single skill folder as MCP resources.
@ -99,7 +111,7 @@ manifest) are exposed to clients\:
**Methods:**
#### `skill_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `skill_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/src/fastmcp/server/providers/skills/skill_provider.py#L271" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
skill_info(self) -> SkillInfo

View file

@ -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,
)

View file

@ -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

View file

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