Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
-----

* Rename `Mcp\Server\Session\Psr16StoreSession` to `Mcp\Server\Session\Psr16SessionStore`
* Introduce `SessionManager` to encapsulate session handling (replaces `SessionFactory`) and move garbage collection logic from `Protocol`.

0.3.0
-----
Expand Down
31 changes: 16 additions & 15 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Mcp\Capability\Registry\Loader\LoaderInterface;
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Capability\RegistryInterface;
use Mcp\Exception\InvalidArgumentException;
use Mcp\JsonRpc\MessageFactory;
use Mcp\Schema\Annotations;
use Mcp\Schema\Enum\ProtocolVersion;
Expand All @@ -34,8 +35,8 @@
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\InMemorySessionStore;
use Mcp\Server\Session\SessionFactory;
use Mcp\Server\Session\SessionFactoryInterface;
use Mcp\Server\Session\SessionManager;
use Mcp\Server\Session\SessionManagerInterface;
use Mcp\Server\Session\SessionStoreInterface;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -66,12 +67,10 @@ final class Builder

private ?DiscovererInterface $discoverer = null;

private ?SessionFactoryInterface $sessionFactory = null;
private ?SessionManagerInterface $sessionManager = null;

private ?SessionStoreInterface $sessionStore = null;

private int $sessionTtl = 3600;

private int $paginationLimit = 50;

private ?string $instructions = null;
Expand Down Expand Up @@ -310,13 +309,15 @@ public function setDiscoverer(DiscovererInterface $discoverer): self
}

public function setSession(
SessionStoreInterface $sessionStore,
SessionFactoryInterface $sessionFactory = new SessionFactory(),
int $ttl = 3600,
?SessionStoreInterface $sessionStore = null,
?SessionManagerInterface $sessionManager = null,
): self {
$this->sessionFactory = $sessionFactory;
$this->sessionStore = $sessionStore;
$this->sessionTtl = $ttl;
$this->sessionManager = $sessionManager;

if (null !== $sessionManager && null !== $sessionStore) {
throw new InvalidArgumentException('Cannot set both SessionStore and SessionManager. Set only one or the other.');
}

return $this;
}
Expand Down Expand Up @@ -504,9 +505,10 @@ public function build(): Server
$loader->load($registry);
}

$sessionTtl = $this->sessionTtl ?? 3600;
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($sessionTtl);
$sessionManager = $this->sessionManager ?? new SessionManager(
$this->sessionStore ?? new InMemorySessionStore(),
$logger,
);
$messageFactory = MessageFactory::make();

$capabilities = $this->serverCapabilities ?? new ServerCapabilities(
Expand Down Expand Up @@ -547,8 +549,7 @@ public function build(): Server
requestHandlers: $requestHandlers,
notificationHandlers: $notificationHandlers,
messageFactory: $messageFactory,
sessionFactory: $sessionFactory,
sessionStore: $sessionStore,
sessionManager: $sessionManager,
logger: $logger,
);

Expand Down
43 changes: 11 additions & 32 deletions src/Server/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
use Mcp\Schema\Request\InitializeRequest;
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Session\SessionFactoryInterface;
use Mcp\Server\Session\SessionInterface;
use Mcp\Server\Session\SessionStoreInterface;
use Mcp\Server\Session\SessionManagerInterface;
use Mcp\Server\Transport\TransportInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -65,8 +64,7 @@ public function __construct(
private readonly array $requestHandlers,
private readonly array $notificationHandlers,
private readonly MessageFactory $messageFactory,
private readonly SessionFactoryInterface $sessionFactory,
private readonly SessionStoreInterface $sessionStore,
private readonly SessionManagerInterface $sessionManager,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
Expand Down Expand Up @@ -106,7 +104,7 @@ public function processInput(TransportInterface $transport, string $input, ?Uuid
{
$this->logger->info('Received message to process.', ['message' => $input]);

$this->gcSessions();
$this->sessionManager->gc();

try {
$messages = $this->messageFactory->create($input);
Expand Down Expand Up @@ -367,7 +365,7 @@ private function queueOutgoing(Request|Notification|Response|Error $message, arr
*/
public function consumeOutgoingMessages(Uuid $sessionId): array
{
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
$session = $this->sessionManager->createWithId($sessionId);
$queue = $session->get(self::SESSION_OUTGOING_QUEUE, []);
$session->set(self::SESSION_OUTGOING_QUEUE, []);
$session->save();
Expand All @@ -386,7 +384,7 @@ public function consumeOutgoingMessages(Uuid $sessionId): array
*/
public function checkResponse(int $requestId, Uuid $sessionId): Response|Error|null
{
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
$session = $this->sessionManager->createWithId($sessionId);
$responseData = $session->get(self::SESSION_RESPONSES.".{$requestId}");

if (null === $responseData) {
Expand Down Expand Up @@ -428,7 +426,7 @@ public function checkResponse(int $requestId, Uuid $sessionId): Response|Error|n
*/
public function getPendingRequests(Uuid $sessionId): array
{
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
$session = $this->sessionManager->createWithId($sessionId);

return $session->get(self::SESSION_PENDING_REQUESTS, []);
}
Expand All @@ -455,7 +453,7 @@ public function handleFiberYield(mixed $yieldedValue, ?Uuid $sessionId): void
return;
}

$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
$session = $this->sessionManager->createWithId($sessionId);

$payloadSessionId = $yieldedValue['session_id'] ?? null;
if (\is_string($payloadSessionId) && $payloadSessionId !== $sessionId->toRfc4122()) {
Expand Down Expand Up @@ -539,7 +537,7 @@ private function resolveSession(TransportInterface $transport, ?Uuid $sessionId,
return null;
}

$session = $this->sessionFactory->create($this->sessionStore);
$session = $this->sessionManager->create();
$this->logger->debug('Created new session for initialize', [
'session_id' => $session->getId()->toRfc4122(),
]);
Expand All @@ -556,41 +554,22 @@ private function resolveSession(TransportInterface $transport, ?Uuid $sessionId,
return null;
}

if (!$this->sessionStore->exists($sessionId)) {
if (!$this->sessionManager->exists($sessionId)) {
$error = Error::forInvalidRequest('Session not found or has expired.');
$this->sendResponse($transport, $error, null, ['status_code' => 404]);

return null;
}

return $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
}

/**
* Run garbage collection on expired sessions.
* Uses the session store's internal TTL configuration.
*/
private function gcSessions(): void
{
if (random_int(0, 100) > 1) {
return;
}

$deletedSessions = $this->sessionStore->gc();
if (!empty($deletedSessions)) {
$this->logger->debug('Garbage collected expired sessions.', [
'count' => \count($deletedSessions),
'session_ids' => array_map(static fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
]);
}
return $this->sessionManager->createWithId($sessionId);
}

/**
* Destroy a specific session.
*/
public function destroySession(Uuid $sessionId): void
{
$this->sessionStore->destroy($sessionId);
$this->sessionManager->destroy($sessionId);
$this->logger->info('Session destroyed.', ['session_id' => $sessionId->toRfc4122()]);
}
}
5 changes: 0 additions & 5 deletions src/Server/Session/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ public function getId(): Uuid
return $this->id;
}

public function getStore(): SessionStoreInterface
{
return $this->store;
}

public function save(): bool
{
return $this->store->write($this->id, json_encode($this->data, \JSON_THROW_ON_ERROR));
Expand Down
32 changes: 0 additions & 32 deletions src/Server/Session/SessionFactory.php

This file was deleted.

5 changes: 0 additions & 5 deletions src/Server/Session/SessionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,4 @@ public function all(): array;
* @param array<string, mixed> $attributes
*/
public function hydrate(array $attributes): void;

/**
* Get the session store instance.
*/
public function getStore(): SessionStoreInterface;
}
69 changes: 69 additions & 0 deletions src/Server/Session/SessionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* 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 Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Uid\Uuid;

/**
* Default implementation of SessionFactoryInterface.
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
class SessionManager implements SessionManagerInterface
{
public function __construct(
private readonly SessionStoreInterface $store,
private readonly LoggerInterface $logger = new NullLogger(),
) {
}

public function create(): SessionInterface
{
return new Session($this->store, Uuid::v4());
}

public function createWithId(Uuid $id): SessionInterface
{
return new Session($this->store, $id);
}

public function exists(Uuid $id): bool
{
return $this->store->exists($id);
}

public function destroy(Uuid $sessionId): bool
{
return $this->store->destroy($sessionId);
}

/**
* Run garbage collection on expired sessions.
* Uses the session store's internal TTL configuration.
*/
public function gc(): void
{
if (random_int(0, 100) > 1) {
return;
}

$deletedSessions = $this->store->gc();
if (!empty($deletedSessions)) {
$this->logger->debug('Garbage collected expired sessions.', [
'count' => \count($deletedSessions),
'session_ids' => array_map(static fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,29 @@
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
interface SessionFactoryInterface
interface SessionManagerInterface
{
/**
* Creates a new session with an auto-generated UUID.
* This is the standard factory method for creating sessions.
*/
public function create(SessionStoreInterface $store): SessionInterface;
public function create(): SessionInterface;

/**
* Creates a session with a specific UUID.
* Use this when you need to reconstruct a session with a known ID.
*/
public function createWithId(Uuid $id, SessionStoreInterface $store): SessionInterface;
public function createWithId(Uuid $id): SessionInterface;

/**
* Checks if a session with the given UUID exists.
*/
public function exists(Uuid $id): bool;

/**
* Destroys the session with the given UUID.
*/
public function destroy(Uuid $sessionId): bool;

public function gc(): void;
}
Loading