Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Request implements Arrayable
*/
public function __construct(
protected array $arguments = [],
protected ?string $sessionId = null
) {
//
}
Expand Down Expand Up @@ -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;
}
}
10 changes: 8 additions & 2 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -249,7 +250,12 @@ protected function handleInitializeMessage(JsonRpcRequest $request, ServerContex
{
$response = (new Initialize)->handle($request, $context);

$this->transport->send($response->toJson());
$this->transport->send($response->toJson(), $this->generateSessionId());
}

protected function generateSessionId(): string
{
return Str::uuid()->toString();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Server/Contracts/Transport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
8 changes: 5 additions & 3 deletions src/Server/Transport/JsonRpcRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function __construct(
public int|string $id,
public string $method,
public array $params,
public ?string $sessionId = null
) {
//
}
Expand All @@ -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'];

Expand All @@ -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,
);
}

Expand All @@ -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);
}
}
11 changes: 11 additions & 0 deletions tests/Feature/Console/StartCommandTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Testing\TestResponse;
use Symfony\Component\Process\Process;

it('can initialize a connection over http', function (): void {
Expand All @@ -11,6 +12,16 @@
expect($response->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);

Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/ArrayTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions tests/Fixtures/ExampleServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ class ExampleServer extends Server
DailyPlanResource::class,
RecentMeetingRecordingResource::class,
];

protected function generateSessionId(): string
{
return 'overridden-'.uniqid();
}
}
11 changes: 11 additions & 0 deletions tests/Unit/Transport/JsonRpcRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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].');
Expand Down
Loading