Skip to content

Commit b4f9be7

Browse files
Pin non-blocking skill reads, drop SSE guidance, and tighten dynamic resource docs
1 parent 702da02 commit b4f9be7

5 files changed

Lines changed: 35 additions & 11 deletions

File tree

‎docs/advanced/extensions.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,15 @@ The client is its own program, and it carries both halves of the client story:
137137

138138
Normally `resources()` is called once, when the server is built. If your resources change while
139139
the server runs, set `resources_are_dynamic = True` and the server asks `resources()` again on
140-
every listing and lookup.
140+
every listing, and on every lookup that a resource registered directly on the server or an
141+
earlier dynamic extension hasn't already answered.
141142

142143
Build the new resources completely before you start returning them, so a request never sees a
143144
half-updated set. Return them as a tuple, and the same tuple until they change, so the server can
144-
look a resource up by URI instead of scanning every resource on each request. If two resources
145-
share a URI, the one registered directly on the server wins,
146-
then dynamic ones in extension order, then templates.
145+
look a resource up by URI instead of scanning every resource on each request. Don't change a
146+
resource's URI inside a tuple you've returned; return a new tuple instead. If two resources share
147+
a URI, the one registered directly on the server wins, then dynamic ones in extension order, then
148+
templates.
147149

148150
To run background work, such as a task that keeps those resources up to date, override
149151
`lifespan()` with an `@asynccontextmanager`. Code before its `yield` runs when the server starts,

‎docs/advanced/skills.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,9 @@ Every 30 seconds here, the catalog calls `reload()`. Automatic refresh is **off
254254
`auto_refresh=True` alone, `refresh_interval` defaults to 60 seconds; it must be a finite number
255255
greater than zero.
256256

257-
There's no task to start and nothing to stop: the timer runs with the server's lifespan, on stdio,
258-
SSE and streamable HTTP. SSE enters the lifespan once per connection, so there the timer runs only
259-
while a client is connected.
257+
There's no task to start and nothing to stop: the timer runs with the server's lifespan. On
258+
streamable HTTP that is the server's whole lifetime. A transport that enters the lifespan once per
259+
connection runs the timer only while a client is connected.
260260

261261
* **A failed refresh changes nothing.** The error is logged and the previous skills keep serving.
262262
The catalog tries again after the next interval.

‎src/mcp/server/extension.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class Extension:
106106
#: Reverse-DNS extension identifier, advertised under `ServerCapabilities.extensions`.
107107
identifier: str
108108
resources_are_dynamic: bool = False
109-
"""When true, `resources()` is consulted on each resource listing or lookup."""
109+
"""When true, `resources()` is called again on each resource listing, and on each lookup not already answered."""
110110

111111
def __init_subclass__(cls, **kwargs: Any) -> None:
112112
super().__init_subclass__(**kwargs)

‎src/mcp/server/mcpserver/resources/resource_manager.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,12 @@ def add_template(
109109
return template
110110

111111
def add_resource_provider(self, provider: Callable[[], Sequence[Resource]]) -> None:
112-
"""Register a dynamic source of concrete resources, consulted on every listing and lookup.
112+
"""Register a dynamic source of concrete resources.
113113
114-
Resources added with `add_resource` take precedence, then providers in the order they were added.
115-
A provider that returns the same tuple again is looked up by URI without rescanning it.
114+
The provider is called on every listing, and on every lookup that a resource added with
115+
`add_resource` or an earlier provider hasn't already answered. A provider that returns the
116+
same tuple again is looked up by URI without rescanning it, so don't change a resource's URI
117+
inside a returned tuple; return a new tuple instead.
116118
"""
117119
self._resource_sources.append(_ResourceSource(provider))
118120

‎tests/server/skills/test_filesystem.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,26 @@ def fstat(fd: int) -> Any:
335335
await FilesystemSkillProvider(tmp_path).read_file("pipe")
336336

337337

338+
async def test_read_file_opens_without_blocking_so_a_fifo_swapped_in_cannot_hang_it(
339+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
340+
) -> None:
341+
"""SDK-defined: opening a real FIFO needs POSIX, so record the open flags to pin the guard everywhere."""
342+
(tmp_path / "SKILL.md").write_bytes(b"x")
343+
flags: list[int] = []
344+
real_open = os.open
345+
346+
def recording_open(path: Any, open_flags: int, *args: Any, **kwargs: Any) -> int:
347+
flags.append(open_flags)
348+
return real_open(path, open_flags, *args, **kwargs)
349+
350+
monkeypatch.setattr(os, "open", recording_open)
351+
352+
assert await FilesystemSkillProvider(tmp_path).read_file("SKILL.md") == b"x"
353+
nonblock = getattr(os, "O_NONBLOCK", 0)
354+
assert len(flags) == 1
355+
assert flags[0] & nonblock == nonblock
356+
357+
338358
async def test_a_root_that_is_itself_a_link_is_served(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
339359
"""SDK-defined: trusted-root link metadata is not refused; simulate it without needing link privileges."""
340360
real = tmp_path / "real"

0 commit comments

Comments
 (0)