Skip to content

Commit ae32064

Browse files
authored
Encapsulate session handling from protocol to new SessionManager (#247)
1 parent a6ed01b commit ae32064

File tree

10 files changed

+444
-200
lines changed

10 files changed

+444
-200
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
77

88
* Rename `Mcp\Server\Session\Psr16StoreSession` to `Mcp\Server\Session\Psr16SessionStore`
99
* Add missing handlers for resource subscribe/unsubscribe and persist subscriptions via session
10+
* Introduce `SessionManager` to encapsulate session handling (replaces `SessionFactory`) and move garbage collection logic from `Protocol`.
1011

1112
0.3.0
1213
-----

src/Server/Builder.php

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Mcp\Capability\Registry\Loader\LoaderInterface;
2424
use Mcp\Capability\Registry\ReferenceHandler;
2525
use Mcp\Capability\RegistryInterface;
26+
use Mcp\Exception\InvalidArgumentException;
2627
use Mcp\JsonRpc\MessageFactory;
2728
use Mcp\Schema\Annotations;
2829
use Mcp\Schema\Enum\ProtocolVersion;
@@ -36,8 +37,8 @@
3637
use Mcp\Server\Resource\SessionSubscriptionManager;
3738
use Mcp\Server\Resource\SubscriptionManagerInterface;
3839
use Mcp\Server\Session\InMemorySessionStore;
39-
use Mcp\Server\Session\SessionFactory;
40-
use Mcp\Server\Session\SessionFactoryInterface;
40+
use Mcp\Server\Session\SessionManager;
41+
use Mcp\Server\Session\SessionManagerInterface;
4142
use Mcp\Server\Session\SessionStoreInterface;
4243
use Psr\Container\ContainerInterface;
4344
use Psr\EventDispatcher\EventDispatcherInterface;
@@ -70,12 +71,10 @@ final class Builder
7071

7172
private ?DiscovererInterface $discoverer = null;
7273

73-
private ?SessionFactoryInterface $sessionFactory = null;
74+
private ?SessionManagerInterface $sessionManager = null;
7475

7576
private ?SessionStoreInterface $sessionStore = null;
7677

77-
private int $sessionTtl = 3600;
78-
7978
private int $paginationLimit = 50;
8079

8180
private ?string $instructions = null;
@@ -321,13 +320,15 @@ public function setResourceSubscriptionManager(SubscriptionManagerInterface $sub
321320
}
322321

323322
public function setSession(
324-
SessionStoreInterface $sessionStore,
325-
SessionFactoryInterface $sessionFactory = new SessionFactory(),
326-
int $ttl = 3600,
323+
?SessionStoreInterface $sessionStore = null,
324+
?SessionManagerInterface $sessionManager = null,
327325
): self {
328-
$this->sessionFactory = $sessionFactory;
329326
$this->sessionStore = $sessionStore;
330-
$this->sessionTtl = $ttl;
327+
$this->sessionManager = $sessionManager;
328+
329+
if (null !== $sessionManager && null !== $sessionStore) {
330+
throw new InvalidArgumentException('Cannot set both SessionStore and SessionManager. Set only one or the other.');
331+
}
331332

332333
return $this;
333334
}
@@ -506,9 +507,10 @@ public function build(): Server
506507
new ArrayLoader($this->tools, $this->resources, $this->resourceTemplates, $this->prompts, $logger, $this->schemaGenerator),
507508
];
508509

509-
$sessionTtl = $this->sessionTtl ?? 3600;
510-
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
511-
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($sessionTtl);
510+
$sessionManager = $this->sessionManager ?? new SessionManager(
511+
$this->sessionStore ?? new InMemorySessionStore(),
512+
$logger,
513+
);
512514

513515
if (null !== $this->discoveryBasePath) {
514516
$discoverer = $this->discoverer ?? $this->createDiscoverer($logger);
@@ -561,8 +563,7 @@ public function build(): Server
561563
requestHandlers: $requestHandlers,
562564
notificationHandlers: $notificationHandlers,
563565
messageFactory: $messageFactory,
564-
sessionFactory: $sessionFactory,
565-
sessionStore: $sessionStore,
566+
sessionManager: $sessionManager,
566567
logger: $logger,
567568
eventDispatcher: $this->eventDispatcher,
568569
);

src/Server/Protocol.php

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@
2525
use Mcp\Schema\Request\InitializeRequest;
2626
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
2727
use Mcp\Server\Handler\Request\RequestHandlerInterface;
28-
use Mcp\Server\Session\SessionFactoryInterface;
2928
use Mcp\Server\Session\SessionInterface;
30-
use Mcp\Server\Session\SessionStoreInterface;
29+
use Mcp\Server\Session\SessionManagerInterface;
3130
use Mcp\Server\Transport\TransportInterface;
3231
use Psr\EventDispatcher\EventDispatcherInterface;
3332
use Psr\Log\LoggerInterface;
@@ -70,8 +69,7 @@ public function __construct(
7069
private readonly array $requestHandlers,
7170
private readonly array $notificationHandlers,
7271
private readonly MessageFactory $messageFactory,
73-
private readonly SessionFactoryInterface $sessionFactory,
74-
private readonly SessionStoreInterface $sessionStore,
72+
private readonly SessionManagerInterface $sessionManager,
7573
private readonly LoggerInterface $logger = new NullLogger(),
7674
private readonly ?EventDispatcherInterface $eventDispatcher = null,
7775
) {
@@ -112,7 +110,7 @@ public function processInput(TransportInterface $transport, string $input, ?Uuid
112110
{
113111
$this->logger->info('Received message to process.', ['message' => $input]);
114112

115-
$this->gcSessions();
113+
$this->sessionManager->gc();
116114

117115
try {
118116
$messages = $this->messageFactory->create($input);
@@ -410,7 +408,7 @@ private function queueOutgoing(Request|Notification|Response|Error $message, arr
410408
*/
411409
public function consumeOutgoingMessages(Uuid $sessionId): array
412410
{
413-
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
411+
$session = $this->sessionManager->createWithId($sessionId);
414412
$queue = $session->get(self::SESSION_OUTGOING_QUEUE, []);
415413
$session->set(self::SESSION_OUTGOING_QUEUE, []);
416414
$session->save();
@@ -429,7 +427,7 @@ public function consumeOutgoingMessages(Uuid $sessionId): array
429427
*/
430428
public function checkResponse(int $requestId, Uuid $sessionId): Response|Error|null
431429
{
432-
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
430+
$session = $this->sessionManager->createWithId($sessionId);
433431
$responseData = $session->get(self::SESSION_RESPONSES.".{$requestId}");
434432

435433
if (null === $responseData) {
@@ -471,7 +469,7 @@ public function checkResponse(int $requestId, Uuid $sessionId): Response|Error|n
471469
*/
472470
public function getPendingRequests(Uuid $sessionId): array
473471
{
474-
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
472+
$session = $this->sessionManager->createWithId($sessionId);
475473

476474
return $session->get(self::SESSION_PENDING_REQUESTS, []);
477475
}
@@ -498,7 +496,7 @@ public function handleFiberYield(mixed $yieldedValue, ?Uuid $sessionId): void
498496
return;
499497
}
500498

501-
$session = $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
499+
$session = $this->sessionManager->createWithId($sessionId);
502500

503501
$payloadSessionId = $yieldedValue['session_id'] ?? null;
504502
if (\is_string($payloadSessionId) && $payloadSessionId !== $sessionId->toRfc4122()) {
@@ -582,7 +580,7 @@ private function resolveSession(TransportInterface $transport, ?Uuid $sessionId,
582580
return null;
583581
}
584582

585-
$session = $this->sessionFactory->create($this->sessionStore);
583+
$session = $this->sessionManager->create();
586584
$this->logger->debug('Created new session for initialize', [
587585
'session_id' => $session->getId()->toRfc4122(),
588586
]);
@@ -599,41 +597,22 @@ private function resolveSession(TransportInterface $transport, ?Uuid $sessionId,
599597
return null;
600598
}
601599

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

606604
return null;
607605
}
608606

609-
return $this->sessionFactory->createWithId($sessionId, $this->sessionStore);
610-
}
611-
612-
/**
613-
* Run garbage collection on expired sessions.
614-
* Uses the session store's internal TTL configuration.
615-
*/
616-
private function gcSessions(): void
617-
{
618-
if (random_int(0, 100) > 1) {
619-
return;
620-
}
621-
622-
$deletedSessions = $this->sessionStore->gc();
623-
if (!empty($deletedSessions)) {
624-
$this->logger->debug('Garbage collected expired sessions.', [
625-
'count' => \count($deletedSessions),
626-
'session_ids' => array_map(static fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
627-
]);
628-
}
607+
return $this->sessionManager->createWithId($sessionId);
629608
}
630609

631610
/**
632611
* Destroy a specific session.
633612
*/
634613
public function destroySession(Uuid $sessionId): void
635614
{
636-
$this->sessionStore->destroy($sessionId);
615+
$this->sessionManager->destroy($sessionId);
637616
$this->logger->info('Session destroyed.', ['session_id' => $sessionId->toRfc4122()]);
638617
}
639618
}

src/Server/Session/Session.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ public function getId(): Uuid
4444
return $this->id;
4545
}
4646

47-
public function getStore(): SessionStoreInterface
48-
{
49-
return $this->store;
50-
}
51-
5247
public function save(): bool
5348
{
5449
return $this->store->write($this->id, json_encode($this->readData(), \JSON_THROW_ON_ERROR));

src/Server/Session/SessionFactory.php

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/Server/Session/SessionInterface.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,4 @@ public function all(): array;
7777
* @param array<string, mixed> $attributes
7878
*/
7979
public function hydrate(array $attributes): void;
80-
81-
/**
82-
* Get the session store instance.
83-
*/
84-
public function getStore(): SessionStoreInterface;
8580
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Server\Session;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Psr\Log\NullLogger;
16+
use Symfony\Component\Uid\Uuid;
17+
18+
/**
19+
* Default implementation of SessionManagerInterface.
20+
*
21+
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
22+
*/
23+
class SessionManager implements SessionManagerInterface
24+
{
25+
public function __construct(
26+
private readonly SessionStoreInterface $store,
27+
private readonly LoggerInterface $logger = new NullLogger(),
28+
) {
29+
}
30+
31+
public function create(): SessionInterface
32+
{
33+
return new Session($this->store, Uuid::v4());
34+
}
35+
36+
public function createWithId(Uuid $id): SessionInterface
37+
{
38+
return new Session($this->store, $id);
39+
}
40+
41+
public function exists(Uuid $id): bool
42+
{
43+
return $this->store->exists($id);
44+
}
45+
46+
public function destroy(Uuid $id): bool
47+
{
48+
return $this->store->destroy($id);
49+
}
50+
51+
/**
52+
* Run garbage collection on expired sessions.
53+
* Uses the session store's internal TTL configuration.
54+
*/
55+
public function gc(): void
56+
{
57+
if (random_int(0, 100) > 1) {
58+
return;
59+
}
60+
61+
$deletedSessions = $this->store->gc();
62+
if (!empty($deletedSessions)) {
63+
$this->logger->debug('Garbage collected expired sessions.', [
64+
'count' => \count($deletedSessions),
65+
'session_ids' => array_map(static fn (Uuid $id) => $id->toRfc4122(), $deletedSessions),
66+
]);
67+
}
68+
}
69+
}

src/Server/Session/SessionFactoryInterface.php renamed to src/Server/Session/SessionManagerInterface.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,29 @@
1919
*
2020
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
2121
*/
22-
interface SessionFactoryInterface
22+
interface SessionManagerInterface
2323
{
2424
/**
2525
* Creates a new session with an auto-generated UUID.
2626
* This is the standard factory method for creating sessions.
2727
*/
28-
public function create(SessionStoreInterface $store): SessionInterface;
28+
public function create(): SessionInterface;
2929

3030
/**
3131
* Creates a session with a specific UUID.
3232
* Use this when you need to reconstruct a session with a known ID.
3333
*/
34-
public function createWithId(Uuid $id, SessionStoreInterface $store): SessionInterface;
34+
public function createWithId(Uuid $id): SessionInterface;
35+
36+
/**
37+
* Checks if a session with the given UUID exists.
38+
*/
39+
public function exists(Uuid $id): bool;
40+
41+
/**
42+
* Destroys the session with the given UUID.
43+
*/
44+
public function destroy(Uuid $id): bool;
45+
46+
public function gc(): void;
3547
}

0 commit comments

Comments
 (0)