Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions src/skillz/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def read_body(self) -> str:
class SkillResourceMetadata(TypedDict):
"""Metadata describing a registered skill resource."""

path: str
relative_path: str
uri: str
runnable: bool
Expand Down Expand Up @@ -568,7 +567,6 @@ def _read_resource() -> str | bytes:

metadata.append(
{
"path": str(resource_path),
"relative_path": relative_display,
"uri": uri,
"runnable": runnable,
Expand Down Expand Up @@ -627,18 +625,14 @@ async def _skill_tool( # type: ignore[unused-ignore]
instructions = bound_skill.read_body()
resource_entries = [
{
"path": entry["path"],
"relative_path": entry["relative_path"],
"uri": entry["uri"],
"runnable": entry["runnable"],
}
for entry in bound_resources
]
resource_uris = [entry["uri"] for entry in resource_entries]
legacy_paths = [entry["path"] for entry in resource_entries]
script_entries = [
{
"path": entry["path"],
"relative_path": entry["relative_path"],
"uri": entry["uri"],
}
Expand Down Expand Up @@ -675,18 +669,24 @@ async def _skill_tool( # type: ignore[unused-ignore]
"""\
Share the `suggested_prompt` with your assistant or embed the
`instructions` text directly alongside the task so the model can apply
the skill as authored. If the instructions mention supporting files,
call `ctx.read_resource` with one of the URIs in `available_resources`
before handing data to the model.
the skill as authored.

IMPORTANT: All skill resources MUST be accessed via the MCP protocol.
If the instructions mention supporting files, use `ctx.read_resource(uri)`
with one of the URIs from `available_resources` to read them. Do not
attempt to access resources via filesystem paths.
"""
).strip(),
"script_execution": {
"call_instructions": textwrap.dedent(
"""\
Invoke this tool again with the `script` parameter set to a path
relative to the skill root (choose from `available_scripts`) and
optionally include `script_payload` (keys: args, env, files, stdin,
workdir).
Invoke this tool again with the `script` parameter set to a relative
path from the skill root (use the `relative_path` from
`available_scripts`). Optionally include `script_payload` (keys: args,
env, files, stdin, workdir).

Use `ctx.read_resource(uri)` to access any non-script resources before
invoking scripts.
"""
).strip(),
"payload_fields": {
Expand All @@ -713,8 +713,7 @@ async def _skill_tool( # type: ignore[unused-ignore]
},
"available_scripts": script_entries,
"available_resources": [
*resource_uris,
*legacy_paths,
entry["uri"] for entry in resource_entries
],
},
},
Expand Down
26 changes: 16 additions & 10 deletions tests/test_run_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ async def test_registers_skill_resources(tmp_path: Path) -> None:
)
expected_entries.append(
{
"path": str(resource_path),
"relative_path": relative.as_posix(),
"uri": uri,
"runnable": runnable,
Expand All @@ -99,9 +98,16 @@ async def test_registers_skill_resources(tmp_path: Path) -> None:

for entry in expected_entries:
uri = entry["uri"]
path = Path(entry["path"])
# Verify we can read the resource via URI
content = await server._resource_manager.read_resource(uri)
file_bytes = path.read_bytes()
# Find the original path from skill.resources for validation
relative = Path(entry["relative_path"])
resource_path = next(
p
for p in skill.resources
if p.relative_to(skill.directory) == relative
)
file_bytes = resource_path.read_bytes()
try:
file_text = file_bytes.decode("utf-8")
except UnicodeDecodeError:
Expand All @@ -124,7 +130,6 @@ async def test_registers_skill_resources(tmp_path: Path) -> None:
]
assert available_scripts == [
{
"path": entry["path"],
"relative_path": entry["relative_path"],
"uri": entry["uri"],
}
Expand All @@ -133,12 +138,13 @@ async def test_registers_skill_resources(tmp_path: Path) -> None:
]

available = payload["usage"]["script_execution"]["available_resources"]
assert available[: len(expected_entries)] == [
entry["uri"] for entry in expected_entries
]
assert available[len(expected_entries) :] == [
entry["path"] for entry in expected_entries
]
assert available == [entry["uri"] for entry in expected_entries]

# Regression test: ensure no absolute paths leak into the payload
import json

payload_json = json.dumps(payload)
assert '"path"' not in payload_json or "relative_path" in payload_json


@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading