Skip to content

Commit 9e0da68

Browse files
committed
Default skill reads to the main resource
1 parent 5e4fb31 commit 9e0da68

5 files changed

Lines changed: 39 additions & 16 deletions

File tree

‎docs/advanced/skills.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ These three validate the server's response against the SEP-2640 conformance rule
6666
it. A name that doesn't match its URI, a digest in the wrong shape, or an incomplete manifest raises
6767
`ValueError` rather than reaching your code.
6868

69-
`read_skill_resource(skill, uri)` fetches a file and verifies it against the manifest in the `Skill`
70-
entry you pass. It returns a `ReadResourceResult` with text or blob content only after the URI,
71-
size, and SHA-256 digest match. Keep that entry while you act on the skill so every file is checked
72-
against the same manifest.
69+
`read_skill_resource(skill)` fetches the skill's `SKILL.md` and verifies it against the manifest in
70+
the `Skill` entry you pass. For a supporting file, pass its URI as a second argument. It returns a
71+
`ReadResourceResult` with text or blob content only after the URI, size, and SHA-256 digest match.
72+
Keep that entry while you act on the skill so every file is checked against the same manifest.
7373

7474
!!! tip
7575
`read_skill_resource` checks the bytes returned by `resources/read` against the held manifest.

‎docs_src/skills/tutorial001_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async def main() -> None:
1212
print(skill.uri, skill.frontmatter["description"])
1313

1414
skill = await catalog.get_skill("skill://git-workflow/SKILL.md")
15-
result = await catalog.read_skill_resource(skill, skill.uri)
15+
result = await catalog.read_skill_resource(skill)
1616
content = result.contents[0]
1717
if isinstance(content, TextResourceContents):
1818
print(content.text)

‎src/mcp/client/skills.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,13 @@ async def get_skill(self, uri: str) -> Skill:
138138
raise ValueError(f"server returned skill {result.skill.uri!r} for requested {uri!r}")
139139
return result.skill
140140

141-
async def read_skill_resource(self, skill: Skill, uri: str) -> ReadResourceResult:
141+
async def read_skill_resource(self, skill: Skill, uri: str | None = None) -> ReadResourceResult:
142142
"""Read and verify one file against a held `Skill` entry.
143143
144-
Pass the entry obtained from `list_skills` or `get_skill`. A dynamic
145-
skill has no manifest to verify; use `Client.read_resource` if you
146-
choose to read its unverified content.
144+
Pass the entry obtained from `list_skills` or `get_skill`. Omit `uri`
145+
to read the skill's `SKILL.md`, or pass a supporting file's URI. A
146+
dynamic skill has no manifest to verify; use `Client.read_resource`
147+
if you choose to read its unverified content.
147148
148149
Raises:
149150
ValueError: If the file is not in the manifest, or the server's
@@ -152,6 +153,7 @@ async def read_skill_resource(self, skill: Skill, uri: str) -> ReadResourceResul
152153
RuntimeError: If the server returns an `InputRequiredResult`; this
153154
method does not drive the input-required loop.
154155
"""
156+
uri = skill.uri if uri is None else uri
155157
if skill.resources == "dynamic":
156158
raise ValueError(f"skill {skill.uri!r} has dynamic resources and cannot be integrity-verified")
157159
if not any(resource.uri == uri for resource in skill.resources):

‎tests/client/test_skills.py‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,33 @@ async def test_read_skill_resource_returns_verified_text() -> None:
209209
"""SDK-defined: a matching manifest allows a fetched text file."""
210210
async with _skills(_server()) as skills:
211211
skill = await skills.get_skill(_SKILL_URI)
212-
result = await skills.read_skill_resource(skill, _SKILL_URI)
212+
result = await skills.read_skill_resource(skill)
213213
contents = result.contents[0]
214214
assert isinstance(contents, TextResourceContents)
215215
assert contents.text == _SKILL_CONTENT
216216

217217

218+
async def test_read_skill_resource_accepts_a_supporting_file_uri() -> None:
219+
"""SEP-2640: a supporting file is checked against the same held manifest."""
220+
uri = "skill://git-workflow/references/GUIDE.md"
221+
content = "# Guide\n"
222+
server = _server()
223+
server.add_resource(TextResource(uri=uri, name="GUIDE.md", text=content))
224+
skill = Skill(
225+
uri=_SKILL_URI,
226+
frontmatter={"name": "git-workflow", "description": "d"},
227+
resources=[
228+
SkillResource(uri=_SKILL_URI, digest=_SKILL_DIGEST, size=len(_SKILL_CONTENT)),
229+
SkillResource(uri=uri, digest=f"sha256:{hashlib.sha256(content.encode()).hexdigest()}", size=len(content)),
230+
],
231+
)
232+
async with _skills(server) as skills:
233+
result = await skills.read_skill_resource(skill, uri)
234+
contents = result.contents[0]
235+
assert isinstance(contents, TextResourceContents)
236+
assert contents.text == content
237+
238+
218239
async def test_read_skill_resource_rejects_a_changed_file() -> None:
219240
"""SEP-2640: a host rejects content that differs from the held manifest."""
220241
stale = Skill(
@@ -224,7 +245,7 @@ async def test_read_skill_resource_rejects_a_changed_file() -> None:
224245
)
225246
async with _skills(_server()) as skills:
226247
with pytest.raises(ValueError) as exc_info:
227-
await skills.read_skill_resource(stale, _SKILL_URI)
248+
await skills.read_skill_resource(stale)
228249
assert str(exc_info.value) == snapshot(
229250
"resource 'skill://git-workflow/SKILL.md' has digest "
230251
"'sha256:7187b725da92d7447b6f50e21d0bf4f3b9db3387acb39239c4a4edc193ddcd4b', "
@@ -247,7 +268,7 @@ async def test_read_skill_resource_rejects_a_dynamic_skill() -> None:
247268
dynamic = Skill(uri=_SKILL_URI, frontmatter={"name": "git-workflow", "description": "d"}, resources="dynamic")
248269
async with _skills(_server()) as skills:
249270
with pytest.raises(ValueError) as exc_info:
250-
await skills.read_skill_resource(dynamic, _SKILL_URI)
271+
await skills.read_skill_resource(dynamic)
251272
assert str(exc_info.value) == snapshot(
252273
"skill 'skill://git-workflow/SKILL.md' has dynamic resources and cannot be integrity-verified"
253274
)
@@ -264,7 +285,7 @@ async def test_read_skill_resource_returns_verified_binary_content() -> None:
264285
resources=[SkillResource(uri=_SKILL_URI, digest=f"sha256:{hashlib.sha256(data).hexdigest()}", size=len(data))],
265286
)
266287
async with _skills(server) as skills:
267-
result = await skills.read_skill_resource(skill, _SKILL_URI)
288+
result = await skills.read_skill_resource(skill)
268289
contents = result.contents[0]
269290
assert isinstance(contents, BlobResourceContents)
270291
assert contents.blob == base64.b64encode(data).decode("ascii")
@@ -294,7 +315,7 @@ async def handler(ctx: ServerRequestContext[Any, Any], params: ReadResourceReque
294315
server.add_request_handler("resources/read", ReadResourceRequestParams, handler)
295316
async with Client(server, extensions=[ClientSkills()]) as client:
296317
with pytest.raises(ValueError) as exc_info:
297-
await client.extension(ClientSkills).read_skill_resource(_skill(), _SKILL_URI)
318+
await client.extension(ClientSkills).read_skill_resource(_skill())
298319
assert str(exc_info.value) == snapshot(
299320
"resource 'skill://git-workflow/SKILL.md' did not return exactly one matching content item"
300321
)
@@ -314,7 +335,7 @@ async def handler(ctx: ServerRequestContext[Any, Any], params: ReadResourceReque
314335
server.add_request_handler("resources/read", ReadResourceRequestParams, handler)
315336
async with Client(server, extensions=[ClientSkills()]) as client:
316337
with pytest.raises(ValueError) as exc_info:
317-
await client.extension(ClientSkills).read_skill_resource(_skill(), _SKILL_URI)
338+
await client.extension(ClientSkills).read_skill_resource(_skill())
318339
assert str(exc_info.value) == snapshot("resource 'skill://git-workflow/SKILL.md' returned invalid base64 content")
319340

320341

‎tests/docs_src/test_skills.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async def test_read_skill_resource_returns_verified_content() -> None:
5353
async with Client(tutorial001.mcp, extensions=[skills]) as client:
5454
catalog = client.extension(Skills)
5555
skill = await catalog.get_skill(tutorial001.SKILL_URI)
56-
result = await catalog.read_skill_resource(skill, skill.uri)
56+
result = await catalog.read_skill_resource(skill)
5757
contents = result.contents[0]
5858
assert isinstance(contents, TextResourceContents)
5959
assert contents.text == tutorial001.SKILL_MD

0 commit comments

Comments
 (0)