feat(server): Rework transport architecture and add StreamableHttpTransport (#49)
* feat(server): Rework transport architecture and add StreamableHttpTransport - `Server::connect()` no longer contains a processing loop. - `TransportInterface` is updated with `setMessageHandler()` and `listen()`. - `StdioTransport` is updated to implement the new interface. - A new, minimal `StreamableHttpTransport` is added for stateless HTTP. * refactor(server): use event-driven message handling * feat(server): Introduce a formal session management system * fix: ensure session elements are preserved when building server (regression from #46) * refactor: consolidate HTTP example to use shared dependencies * feat(server): Enhance message handling with session support - Updated `TransportInterface` to use `onMessage` for handling incoming messages with session IDs. - Refactored `Server`, `Handler`, and transport classes to accommodate session management using `Uuid`. - Introduced methods for creating sessions with auto-generated and specific UUIDs in `SessionFactory` and `SessionFactoryInterface`. * feat(server): Integrate session management in message handling - Added session support to the `Server` and `Handler` classes, allowing for session data to be managed during message processing. - Updated `TransportInterface` to include session context in the `send` method. - Refactored various request handlers to utilize session information, ensuring proper session handling for incoming requests. - Introduced a file-based session store for persistent session data management * feat: Update session builder method to use set* instead of with* * feat: Fix test compatibility with new session management architecture * chore: apply code style fixes * fix: remove deprecated SSE transport and resolve PHPStan issues
This commit is contained in:
parent
b1e54f1f86
commit
719262b15d
1 changed files with 1614 additions and 406 deletions
157
src/Server/Session/FileSessionStore.php
Normal file
157
src/Server/Session/FileSessionStore.php
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the official PHP MCP SDK.
|
||||
*
|
||||
* A collaboration between Symfony and the PHP Foundation.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Mcp\Server\Session;
|
||||
|
||||
use Mcp\Server\NativeClock;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
/**
|
||||
* File-based session store.
|
||||
* Stores each session as a file named by the RFC4122 UUID, with the payload.
|
||||
*/
|
||||
class FileSessionStore implements SessionStoreInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $directory,
|
||||
private readonly int $ttl = 3600,
|
||||
private readonly ClockInterface $clock = new NativeClock(),
|
||||
) {
|
||||
if (!is_dir($this->directory)) {
|
||||
@mkdir($this->directory, 0775, true);
|
||||
}
|
||||
|
||||
if (!is_dir($this->directory) || !is_writable($this->directory)) {
|
||||
throw new \RuntimeException(\sprintf('Session directory "%s" is not writable.', $this->directory));
|
||||
}
|
||||
}
|
||||
|
||||
public function exists(Uuid $id): bool
|
||||
{
|
||||
$path = $this->pathFor($id);
|
||||
|
||||
if (!is_file($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mtime = @filemtime($path) ?: 0;
|
||||
|
||||
return ($this->clock->now()->getTimestamp() - $mtime) <= $this->ttl;
|
||||
}
|
||||
|
||||
public function read(Uuid $sessionId): string|false
|
||||
{
|
||||
$path = $this->pathFor($sessionId);
|
||||
|
||||
if (!is_file($path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mtime = @filemtime($path) ?: 0;
|
||||
if (($this->clock->now()->getTimestamp() - $mtime) > $this->ttl) {
|
||||
@unlink($path);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = @file_get_contents($path);
|
||||
if (false === $data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function write(Uuid $sessionId, string $data): bool
|
||||
{
|
||||
$path = $this->pathFor($sessionId);
|
||||
|
||||
$tmp = $path.'.tmp';
|
||||
if (false === @file_put_contents($tmp, $data, \LOCK_EX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Atomic move
|
||||
if (!@rename($tmp, $path)) {
|
||||
// Fallback if rename fails cross-device
|
||||
if (false === @copy($tmp, $path)) {
|
||||
@unlink($tmp);
|
||||
|
||||
return false;
|
||||
}
|
||||
@unlink($tmp);
|
||||
}
|
||||
|
||||
@touch($path, $this->clock->now()->getTimestamp());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy(Uuid $sessionId): bool
|
||||
{
|
||||
$path = $this->pathFor($sessionId);
|
||||
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove sessions older than the configured TTL.
|
||||
* Returns an array of deleted session IDs (UUID instances).
|
||||
*/
|
||||
public function gc(): array
|
||||
{
|
||||
$deleted = [];
|
||||
$now = $this->clock->now()->getTimestamp();
|
||||
|
||||
$dir = @opendir($this->directory);
|
||||
if (false === $dir) {
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
while (($entry = readdir($dir)) !== false) {
|
||||
// Skip dot entries
|
||||
if ('.' === $entry || '..' === $entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->directory.\DIRECTORY_SEPARATOR.$entry;
|
||||
if (!is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mtime = @filemtime($path) ?: 0;
|
||||
if (($now - $mtime) > $this->ttl) {
|
||||
@unlink($path);
|
||||
try {
|
||||
$deleted[] = Uuid::fromString($entry);
|
||||
} catch (\Throwable) {
|
||||
// ignore non-UUID file names
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir($dir);
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
private function pathFor(Uuid $id): string
|
||||
{
|
||||
return $this->directory.\DIRECTORY_SEPARATOR.$id->toRfc4122();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue