From 78ae8bbc0eb23c7e8eb71f3aba6e2c285fcda48c Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Sun, 21 Sep 2025 08:57:52 +0100 Subject: [PATCH 1/4] 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 --- src/Request.php | 6 ++++++ src/Server.php | 6 ++++-- src/Server/Contracts/Transport.php | 2 +- src/Server/Transport/JsonRpcRequest.php | 8 +++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Request.php b/src/Request.php index 2a8a022..5fdb28d 100644 --- a/src/Request.php +++ b/src/Request.php @@ -23,6 +23,7 @@ class Request implements Arrayable */ public function __construct( protected array $arguments = [], + protected ?string $sessionId = null ) { // } @@ -81,4 +82,9 @@ public function user(?string $guard = null): ?Authenticatable return call_user_func($auth->userResolver(), $guard); } + + public function sessionId(): ?string + { + return $this->sessionId; + } } diff --git a/src/Server.php b/src/Server.php index 4581ef0..3444d2f 100644 --- a/src/Server.php +++ b/src/Server.php @@ -5,6 +5,7 @@ namespace Laravel\Mcp; use Illuminate\Container\Container; +use Illuminate\Support\Str; use Laravel\Mcp\Server\Contracts\Method; use Laravel\Mcp\Server\Contracts\Transport; use Laravel\Mcp\Server\Exceptions\JsonRpcException; @@ -161,7 +162,7 @@ public function handle(string $rawMessage): void } $request = isset($jsonRequest['id']) - ? JsonRpcRequest::from($jsonRequest) + ? JsonRpcRequest::from($jsonRequest, $this->transport->sessionId()) : JsonRpcNotification::from($jsonRequest); if ($request instanceof JsonRpcNotification) { @@ -248,8 +249,9 @@ protected function handleMessage(JsonRpcRequest $request, ServerContext $context protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void { $response = (new Initialize)->handle($request, $context); + $sessionId = Str::uuid()->toString(); - $this->transport->send($response->toJson()); + $this->transport->send($response->toJson(), $sessionId); } /** diff --git a/src/Server/Contracts/Transport.php b/src/Server/Contracts/Transport.php index e5d31a4..60417ff 100644 --- a/src/Server/Contracts/Transport.php +++ b/src/Server/Contracts/Transport.php @@ -12,7 +12,7 @@ public function onReceive(Closure $handler): void; public function run(); // @phpstan-ignore-line - public function send(string $message): void; + public function send(string $message, ?string $sessionId = null): void; public function sessionId(): ?string; diff --git a/src/Server/Transport/JsonRpcRequest.php b/src/Server/Transport/JsonRpcRequest.php index 1d4a5eb..1ee3287 100644 --- a/src/Server/Transport/JsonRpcRequest.php +++ b/src/Server/Transport/JsonRpcRequest.php @@ -16,6 +16,7 @@ public function __construct( public int|string $id, public string $method, public array $params, + public ?string $sessionId = null ) { // } @@ -25,7 +26,7 @@ public function __construct( * * @throws JsonRpcException */ - public static function from(array $jsonRequest): static + public static function from(array $jsonRequest, ?string $sessionId = null): static { $requestId = $jsonRequest['id']; @@ -44,7 +45,8 @@ public static function from(array $jsonRequest): static return new static( id: $requestId, method: $jsonRequest['method'], - params: $jsonRequest['params'] ?? [] + params: $jsonRequest['params'] ?? [], + sessionId: $sessionId, ); } @@ -60,6 +62,6 @@ public function get(string $key, mixed $default = null): mixed public function toRequest(): Request { - return new Request($this->params['arguments'] ?? []); + return new Request($this->params['arguments'] ?? [], $this->sessionId); } } From cc245de99e3fd81aff5f636d0191f09ebdddd57b Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Sun, 21 Sep 2025 08:59:20 +0100 Subject: [PATCH 2/4] fix: relax Transport contract for backwards compatibility --- src/Server/Contracts/Transport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server/Contracts/Transport.php b/src/Server/Contracts/Transport.php index 60417ff..e5d31a4 100644 --- a/src/Server/Contracts/Transport.php +++ b/src/Server/Contracts/Transport.php @@ -12,7 +12,7 @@ public function onReceive(Closure $handler): void; public function run(); // @phpstan-ignore-line - public function send(string $message, ?string $sessionId = null): void; + public function send(string $message): void; public function sessionId(): ?string; From 4e4379b8b61541042476a14247940feb35785e3e Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Sun, 21 Sep 2025 09:32:32 +0100 Subject: [PATCH 3/4] feat: add sessionid to transport contract --- src/Server/Contracts/Transport.php | 2 +- tests/Fixtures/ArrayTransport.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Server/Contracts/Transport.php b/src/Server/Contracts/Transport.php index e5d31a4..60417ff 100644 --- a/src/Server/Contracts/Transport.php +++ b/src/Server/Contracts/Transport.php @@ -12,7 +12,7 @@ public function onReceive(Closure $handler): void; public function run(); // @phpstan-ignore-line - public function send(string $message): void; + public function send(string $message, ?string $sessionId = null): void; public function sessionId(): ?string; diff --git a/tests/Fixtures/ArrayTransport.php b/tests/Fixtures/ArrayTransport.php index 55251e7..3fdf40a 100644 --- a/tests/Fixtures/ArrayTransport.php +++ b/tests/Fixtures/ArrayTransport.php @@ -29,7 +29,7 @@ public function run(): void // } - public function send(string $message): void + public function send(string $message, ?string $sessionId = null): void { $this->sent[] = $message; } From 3d29bcd3f1ba39ef99a369c789fd483e4c2d76bc Mon Sep 17 00:00:00 2001 From: Ashley Hindle Date: Sun, 21 Sep 2025 10:08:44 +0100 Subject: [PATCH 4/4] feat: allow sessionid generation extension Adds basic tests --- src/Server.php | 8 ++++++-- tests/Feature/Console/StartCommandTest.php | 11 +++++++++++ tests/Fixtures/ExampleServer.php | 5 +++++ tests/Unit/Transport/JsonRpcRequestTest.php | 11 +++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Server.php b/src/Server.php index 3444d2f..f7bdce2 100644 --- a/src/Server.php +++ b/src/Server.php @@ -249,9 +249,13 @@ protected function handleMessage(JsonRpcRequest $request, ServerContext $context protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void { $response = (new Initialize)->handle($request, $context); - $sessionId = Str::uuid()->toString(); - $this->transport->send($response->toJson(), $sessionId); + $this->transport->send($response->toJson(), $this->generateSessionId()); + } + + protected function generateSessionId(): string + { + return Str::uuid()->toString(); } /** diff --git a/tests/Feature/Console/StartCommandTest.php b/tests/Feature/Console/StartCommandTest.php index a2003c1..b867cc0 100644 --- a/tests/Feature/Console/StartCommandTest.php +++ b/tests/Feature/Console/StartCommandTest.php @@ -1,6 +1,7 @@ json())->toEqual(expectedInitializeResponse()); }); +it('receives a session id over http', function (): void { + /** @var TestResponse $response */ + $response = $this->postJson('test-mcp', initializeMessage()); + + $response->assertHeader('Mcp-Session-Id'); + + // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management + expect($response->headers->get('Mcp-Session-Id'))->toMatch('/^[\x21-\x7E]+$/'); +}); + it('can list resources over http', function (): void { $sessionId = initializeHttpConnection($this); diff --git a/tests/Fixtures/ExampleServer.php b/tests/Fixtures/ExampleServer.php index 9aa39fa..926f9b2 100644 --- a/tests/Fixtures/ExampleServer.php +++ b/tests/Fixtures/ExampleServer.php @@ -16,4 +16,9 @@ class ExampleServer extends Server DailyPlanResource::class, RecentMeetingRecordingResource::class, ]; + + protected function generateSessionId(): string + { + return 'overridden-'.uniqid(); + } } diff --git a/tests/Unit/Transport/JsonRpcRequestTest.php b/tests/Unit/Transport/JsonRpcRequestTest.php index ed8f364..e234f4b 100644 --- a/tests/Unit/Transport/JsonRpcRequestTest.php +++ b/tests/Unit/Transport/JsonRpcRequestTest.php @@ -22,6 +22,17 @@ ->and($request->params)->toEqual(['name' => 'echo', 'arguments' => ['message' => 'Hello, world!']]); }); +it('stores session id when provided', function (): void { + $sessionId = 'i-am-your-session-luke'; + $request = JsonRpcRequest::from([ + 'jsonrpc' => '2.0', + 'id' => 1, + 'method' => 'tools/call', + ], $sessionId); + + expect($request->sessionId)->toBe($sessionId); +}); + it('throws exception for missing jsonrpc version', function (): void { $this->expectException(JsonRpcException::class); $this->expectExceptionMessage('Invalid Request: The [jsonrpc] member must be exactly [2.0].');