Skip to content

Commit 40ebb80

Browse files
feat(server): Introduce a formal session management system
1 parent 3669423 commit 40ebb80

File tree

11 files changed

+451
-12
lines changed

11 files changed

+451
-12
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"ext-fileinfo": "*",
2323
"opis/json-schema": "^2.4",
2424
"phpdocumentor/reflection-docblock": "^5.6",
25+
"psr/clock": "^1.0",
2526
"psr/container": "^2.0",
2627
"psr/event-dispatcher": "^1.0",
2728
"psr/http-factory": "^1.1",

src/Server.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Mcp\JsonRpc\Handler;
1515
use Mcp\Server\ServerBuilder;
16+
use Mcp\Server\Session\SessionFactoryInterface;
17+
use Mcp\Server\Session\SessionStoreInterface;
1618
use Mcp\Server\TransportInterface;
1719
use Psr\Log\LoggerInterface;
1820
use Psr\Log\NullLogger;
@@ -24,6 +26,9 @@ final class Server
2426
{
2527
public function __construct(
2628
private readonly Handler $jsonRpcHandler,
29+
private readonly SessionFactoryInterface $sessionFactory,
30+
private readonly SessionStoreInterface $sessionStore,
31+
private readonly int $sessionTtl,
2732
private readonly LoggerInterface $logger = new NullLogger(),
2833
) {}
2934

@@ -45,10 +50,10 @@ public function connect(TransportInterface $transport): void
4550
});
4651
}
4752

48-
private function handleMessage(string $rawMessage, TransportInterface $transport): void
53+
private function handleMessage(string $message, TransportInterface $transport): void
4954
{
5055
try {
51-
foreach ($this->jsonRpcHandler->process($rawMessage) as $response) {
56+
foreach ($this->jsonRpcHandler->process($message) as $response) {
5257
if (null === $response) {
5358
continue;
5459
}
@@ -57,7 +62,7 @@ private function handleMessage(string $rawMessage, TransportInterface $transport
5762
}
5863
} catch (\JsonException $e) {
5964
$this->logger->error('Failed to encode response to JSON.', [
60-
'message' => $rawMessage,
65+
'message' => $message,
6166
'exception' => $e,
6267
]);
6368
}

src/Server/NativeClock.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server;
6+
7+
use Psr\Clock\ClockInterface;
8+
use DateTimeImmutable;
9+
10+
class NativeClock implements ClockInterface
11+
{
12+
public function now(): DateTimeImmutable
13+
{
14+
return new DateTimeImmutable();
15+
}
16+
}

src/Server/ServerBuilder.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@
3232
use Mcp\Schema\Tool;
3333
use Mcp\Schema\ToolAnnotations;
3434
use Mcp\Server;
35+
use Mcp\Server\Session\SessionFactory;
36+
use Mcp\Server\Session\InMemorySessionStore;
37+
use Mcp\Server\Session\SessionFactoryInterface;
38+
use Mcp\Server\Session\SessionStoreInterface;
3539
use Psr\Container\ContainerInterface;
3640
use Psr\EventDispatcher\EventDispatcherInterface;
3741
use Psr\Log\LoggerInterface;
3842
use Psr\Log\NullLogger;
39-
use Psr\SimpleCache\CacheInterface;
4043

4144
/**
4245
* @author Kyrian Obikwelu <[email protected]>
@@ -47,12 +50,15 @@ final class ServerBuilder
4750

4851
private ?LoggerInterface $logger = null;
4952

50-
private ?CacheInterface $cache = null;
5153

5254
private ?EventDispatcherInterface $eventDispatcher = null;
5355

5456
private ?ContainerInterface $container = null;
5557

58+
private ?SessionFactoryInterface $sessionFactory = null;
59+
private ?SessionStoreInterface $sessionStore = null;
60+
private ?int $sessionTtl = 3600;
61+
5662
private ?int $paginationLimit = 50;
5763

5864
private ?string $instructions = null;
@@ -160,6 +166,18 @@ public function withContainer(ContainerInterface $container): self
160166
return $this;
161167
}
162168

169+
public function withSession(
170+
SessionFactoryInterface $sessionFactory,
171+
SessionStoreInterface $sessionStore,
172+
int $ttl = 3600
173+
): self {
174+
$this->sessionFactory = $sessionFactory;
175+
$this->sessionStore = $sessionStore;
176+
$this->sessionTtl = $ttl;
177+
178+
return $this;
179+
}
180+
163181
public function withDiscovery(
164182
string $basePath,
165183
array $scanDirs = ['.', 'src'],
@@ -222,6 +240,9 @@ public function build(): Server
222240
$container = $this->container ?? new Container();
223241
$registry = new Registry(new ReferenceHandler($container), $this->eventDispatcher, $logger);
224242

243+
$sessionFactory = $this->sessionFactory ?? new SessionFactory();
244+
$sessionStore = $this->sessionStore ?? new InMemorySessionStore($this->sessionTtl);
245+
225246
$this->registerManualElements($registry, $logger);
226247

227248
if (null !== $this->discoveryBasePath) {
@@ -231,6 +252,9 @@ public function build(): Server
231252

232253
return new Server(
233254
Handler::make($registry, $this->serverInfo, $logger),
255+
$sessionFactory,
256+
$sessionStore,
257+
$this->sessionTtl,
234258
$logger,
235259
);
236260
}
@@ -254,7 +278,7 @@ private function registerManualElements(Registry $registry, LoggerInterface $log
254278
$reflection = HandlerResolver::resolve($data['handler']);
255279

256280
if ($reflection instanceof \ReflectionFunction) {
257-
$name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
281+
$name = $data['name'] ?? 'closure_tool_' . spl_object_id($data['handler']);
258282
$description = $data['description'] ?? null;
259283
} else {
260284
$classShortName = $reflection->getDeclaringClass()->getShortName();
@@ -284,7 +308,7 @@ private function registerManualElements(Registry $registry, LoggerInterface $log
284308
$reflection = HandlerResolver::resolve($data['handler']);
285309

286310
if ($reflection instanceof \ReflectionFunction) {
287-
$name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
311+
$name = $data['name'] ?? 'closure_resource_' . spl_object_id($data['handler']);
288312
$description = $data['description'] ?? null;
289313
} else {
290314
$classShortName = $reflection->getDeclaringClass()->getShortName();
@@ -317,7 +341,7 @@ private function registerManualElements(Registry $registry, LoggerInterface $log
317341
$reflection = HandlerResolver::resolve($data['handler']);
318342

319343
if ($reflection instanceof \ReflectionFunction) {
320-
$name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
344+
$name = $data['name'] ?? 'closure_template_' . spl_object_id($data['handler']);
321345
$description = $data['description'] ?? null;
322346
} else {
323347
$classShortName = $reflection->getDeclaringClass()->getShortName();
@@ -350,7 +374,7 @@ private function registerManualElements(Registry $registry, LoggerInterface $log
350374
$reflection = HandlerResolver::resolve($data['handler']);
351375

352376
if ($reflection instanceof \ReflectionFunction) {
353-
$name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
377+
$name = $data['name'] ?? 'closure_prompt_' . spl_object_id($data['handler']);
354378
$description = $data['description'] ?? null;
355379
} else {
356380
$classShortName = $reflection->getDeclaringClass()->getShortName();
@@ -371,7 +395,7 @@ private function registerManualElements(Registry $registry, LoggerInterface $log
371395
continue;
372396
}
373397

374-
$paramTag = $paramTags['$'.$param->getName()] ?? null;
398+
$paramTag = $paramTags['$' . $param->getName()] ?? null;
375399
$arguments[] = new PromptArgument(
376400
$param->getName(),
377401
$paramTag ? trim((string) $paramTag->getDescription()) : null,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Session;
6+
7+
use Mcp\Server\Session\SessionStoreInterface;
8+
use Mcp\Server\NativeClock;
9+
use Psr\Clock\ClockInterface;
10+
use Symfony\Component\Uid\Uuid;
11+
12+
class InMemorySessionStore implements SessionStoreInterface
13+
{
14+
/**
15+
* @var array<string, array{ data: array, timestamp: int }>
16+
*/
17+
protected array $store = [];
18+
19+
public function __construct(
20+
protected readonly int $ttl = 3600,
21+
protected readonly ClockInterface $clock = new NativeClock(),
22+
) {}
23+
24+
public function read(Uuid $sessionId): string|false
25+
{
26+
$session = $this->store[$sessionId->toRfc4122()] ?? '';
27+
if ($session === '') {
28+
return false;
29+
}
30+
31+
$currentTimestamp = $this->clock->now()->getTimestamp();
32+
33+
if ($currentTimestamp - $session['timestamp'] > $this->ttl) {
34+
unset($this->store[$sessionId]);
35+
return false;
36+
}
37+
38+
return $session['data'];
39+
}
40+
41+
public function write(Uuid $sessionId, string $data): bool
42+
{
43+
$this->store[$sessionId->toRfc4122()] = [
44+
'data' => $data,
45+
'timestamp' => $this->clock->now()->getTimestamp(),
46+
];
47+
48+
return true;
49+
}
50+
51+
public function destroy(Uuid $sessionId): bool
52+
{
53+
if (isset($this->store[$sessionId->toRfc4122()])) {
54+
unset($this->store[$sessionId]);
55+
}
56+
57+
return true;
58+
}
59+
60+
public function gc(int $maxLifetime): array
61+
{
62+
$currentTimestamp = $this->clock->now()->getTimestamp();
63+
$deletedSessions = [];
64+
65+
foreach ($this->store as $sessionId => $session) {
66+
$sessionId = Uuid::fromString($sessionId);
67+
if ($currentTimestamp - $session['timestamp'] > $maxLifetime) {
68+
unset($this->store[$sessionId->toRfc4122()]);
69+
$deletedSessions[] = $sessionId;
70+
}
71+
}
72+
73+
return $deletedSessions;
74+
}
75+
}

0 commit comments

Comments
 (0)