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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* Fix a resource whose text is not valid UTF-8 (e.g. a Latin-1 `.txt` file) failing `resources/read` with an internal error or an uncaught `JsonException`: `ResourceResultFormatter` now sends it as a blob with the same MIME type.
* [BC Break] Answer a request of a known method whose params do not fit it (e.g. `tools/call` without `name`) with `-32602` (Invalid params) instead of `-32600` (Invalid request), as JSON-RPC requires. `Request::fromArray()` throws the new `InvalidParamsException`, a subclass of `InvalidArgumentException`, and `InvalidInputMessageException::isInvalidParams()` tells the protocols which code to send. A malformed envelope and an unknown method keep their codes.
* Fix a modern-era result that cannot be encoded as JSON, e.g. tool or resource text that is not valid UTF-8, escaping as an uncaught `JsonException` or ending a stream without an answer: the client gets `-32603`, with HTTP status `500` or as the last frame of a stream, and the failure is logged and dispatched as an `ErrorEvent`. Outside a stream, an error message that is not valid UTF-8 has the invalid bytes replaced.
* Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to a `copy()` that readers can see half done, but on Windows retries the move for a moment, and `write()` returns `false` if the move fails.
* Fix `FileSessionStore` serving a partial session on Windows when a write could not replace the session file: it no longer falls back to a `copy()` that readers can see half done, but on Windows retries the move for up to a second while a reader or another writer holds the file, and `write()` returns `false` if the move fails.
* Fix a stdio server on Windows ending by itself after about 13.5 hours without input, when `fgets()` on the idle pipe reported a false EOF: `StdioTransport` now reads only once `stream_select()` reports input, which also keeps the run loop going while idle. Requires PHP 8.5, as `stream_select()` on Windows reports an idle pipe as readable before that.
* Fix `StdioTransport` throwing a `ValueError` on an input that cannot be selected, like `php://memory`.
* Fix a stdio server never answering a message that reaches it in several reads, such as a tool call with large arguments: `StdioTransport` handled each read as a message and answered the parts with `-32700`. It now waits for the newline.
Expand Down
19 changes: 11 additions & 8 deletions src/Server/Session/FileSessionStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
class FileSessionStore implements SessionStoreInterface
{
private const WINDOWS_MOVE_ATTEMPTS = 10;
private const WINDOWS_MOVE_BUDGET_NS = 1_000_000_000;

public function __construct(
private readonly string $directory,
Expand Down Expand Up @@ -259,18 +259,21 @@ private function deleteExpiredLockFile(string $path, int $now): void

private function moveIntoPlace(string $tmp, string $path): bool
{
// Overlapping writers of a session can briefly deny each other the replace on Windows.
$attempts = 'Windows' === \PHP_OS_FAMILY ? self::WINDOWS_MOVE_ATTEMPTS : 1;
if ('Windows' !== \PHP_OS_FAMILY) {
return @rename($tmp, $path);
}

for ($attempt = 1; $attempt < $attempts; ++$attempt) {
if (@rename($tmp, $path)) {
return true;
// Windows refuses to replace a file while anyone, even a reader, holds it open.
$deadline = hrtime(true) + self::WINDOWS_MOVE_BUDGET_NS;
while (!@rename($tmp, $path)) {
if (hrtime(true) >= $deadline) {
return false;
}

usleep($attempt * 1000);
usleep(1000);
}

return @rename($tmp, $path);
return true;
}

private function isTemporaryFile(string $entry): bool
Expand Down
Loading