Skip to content

Commit 4a8286a

Browse files
authored
add basic sessionid support (#58)
* feat: add session support Generate an initial session id on 'initialize', then put it in the Mcp\Request so primitives can make use of it In future we should add a Session object like HTTP, with differently backed sessions, but this is useful enough for now * fix: relax Transport contract for backwards compatibility * feat: add sessionid to transport contract * feat: allow sessionid generation extension Adds basic tests
1 parent 07366b9 commit 4a8286a

File tree

8 files changed

+48
-7
lines changed

8 files changed

+48
-7
lines changed

src/Request.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Request implements Arrayable
2323
*/
2424
public function __construct(
2525
protected array $arguments = [],
26+
protected ?string $sessionId = null
2627
) {
2728
//
2829
}
@@ -81,4 +82,9 @@ public function user(?string $guard = null): ?Authenticatable
8182

8283
return call_user_func($auth->userResolver(), $guard);
8384
}
85+
86+
public function sessionId(): ?string
87+
{
88+
return $this->sessionId;
89+
}
8490
}

src/Server.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Laravel\Mcp;
66

77
use Illuminate\Container\Container;
8+
use Illuminate\Support\Str;
89
use Laravel\Mcp\Server\Contracts\Method;
910
use Laravel\Mcp\Server\Contracts\Transport;
1011
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
@@ -163,7 +164,7 @@ public function handle(string $rawMessage): void
163164
}
164165

165166
$request = isset($jsonRequest['id'])
166-
? JsonRpcRequest::from($jsonRequest)
167+
? JsonRpcRequest::from($jsonRequest, $this->transport->sessionId())
167168
: JsonRpcNotification::from($jsonRequest);
168169

169170
if ($request instanceof JsonRpcNotification) {
@@ -251,7 +252,12 @@ protected function handleInitializeMessage(JsonRpcRequest $request, ServerContex
251252
{
252253
$response = (new Initialize)->handle($request, $context);
253254

254-
$this->transport->send($response->toJson());
255+
$this->transport->send($response->toJson(), $this->generateSessionId());
256+
}
257+
258+
protected function generateSessionId(): string
259+
{
260+
return Str::uuid()->toString();
255261
}
256262

257263
/**

src/Server/Contracts/Transport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function onReceive(Closure $handler): void;
1212

1313
public function run(); // @phpstan-ignore-line
1414

15-
public function send(string $message): void;
15+
public function send(string $message, ?string $sessionId = null): void;
1616

1717
public function sessionId(): ?string;
1818

src/Server/Transport/JsonRpcRequest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public function __construct(
1616
public int|string $id,
1717
public string $method,
1818
public array $params,
19+
public ?string $sessionId = null
1920
) {
2021
//
2122
}
@@ -25,7 +26,7 @@ public function __construct(
2526
*
2627
* @throws JsonRpcException
2728
*/
28-
public static function from(array $jsonRequest): static
29+
public static function from(array $jsonRequest, ?string $sessionId = null): static
2930
{
3031
$requestId = $jsonRequest['id'];
3132

@@ -44,7 +45,8 @@ public static function from(array $jsonRequest): static
4445
return new static(
4546
id: $requestId,
4647
method: $jsonRequest['method'],
47-
params: $jsonRequest['params'] ?? []
48+
params: $jsonRequest['params'] ?? [],
49+
sessionId: $sessionId,
4850
);
4951
}
5052

@@ -60,6 +62,6 @@ public function get(string $key, mixed $default = null): mixed
6062

6163
public function toRequest(): Request
6264
{
63-
return new Request($this->params['arguments'] ?? []);
65+
return new Request($this->params['arguments'] ?? [], $this->sessionId);
6466
}
6567
}

tests/Feature/Console/StartCommandTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Illuminate\Support\Facades\Route;
4+
use Illuminate\Testing\TestResponse;
45
use Symfony\Component\Process\Process;
56

67
it('can initialize a connection over http', function (): void {
@@ -11,6 +12,16 @@
1112
expect($response->json())->toEqual(expectedInitializeResponse());
1213
});
1314

15+
it('receives a session id over http', function (): void {
16+
/** @var TestResponse $response */
17+
$response = $this->postJson('test-mcp', initializeMessage());
18+
19+
$response->assertHeader('Mcp-Session-Id');
20+
21+
// https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management
22+
expect($response->headers->get('Mcp-Session-Id'))->toMatch('/^[\x21-\x7E]+$/');
23+
});
24+
1425
it('can list resources over http', function (): void {
1526
$sessionId = initializeHttpConnection($this);
1627

tests/Fixtures/ArrayTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function run(): void
2929
//
3030
}
3131

32-
public function send(string $message): void
32+
public function send(string $message, ?string $sessionId = null): void
3333
{
3434
$this->sent[] = $message;
3535
}

tests/Fixtures/ExampleServer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ class ExampleServer extends Server
1616
DailyPlanResource::class,
1717
RecentMeetingRecordingResource::class,
1818
];
19+
20+
protected function generateSessionId(): string
21+
{
22+
return 'overridden-'.uniqid();
23+
}
1924
}

tests/Unit/Transport/JsonRpcRequestTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
->and($request->params)->toEqual(['name' => 'echo', 'arguments' => ['message' => 'Hello, world!']]);
2323
});
2424

25+
it('stores session id when provided', function (): void {
26+
$sessionId = 'i-am-your-session-luke';
27+
$request = JsonRpcRequest::from([
28+
'jsonrpc' => '2.0',
29+
'id' => 1,
30+
'method' => 'tools/call',
31+
], $sessionId);
32+
33+
expect($request->sessionId)->toBe($sessionId);
34+
});
35+
2536
it('throws exception for missing jsonrpc version', function (): void {
2637
$this->expectException(JsonRpcException::class);
2738
$this->expectExceptionMessage('Invalid Request: The [jsonrpc] member must be exactly [2.0].');

0 commit comments

Comments
 (0)