diff --git a/composer.json b/composer.json index 199d633..d893ca4 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,7 @@ "pint --test", "rector --dry-run" ], - "test:unit": "pest --ci --coverage --min=91.0", + "test:unit": "pest --ci --coverage --min=91.3", "test:types": "phpstan", "test": [ "@test:lint", diff --git a/src/Request.php b/src/Request.php index 5fdb28d..5801b83 100644 --- a/src/Request.php +++ b/src/Request.php @@ -87,4 +87,17 @@ public function sessionId(): ?string { return $this->sessionId; } + + /** + * @param array $arguments + */ + public function setArguments(array $arguments): void + { + $this->arguments = $arguments; + } + + public function setSessionId(?string $sessionId): void + { + $this->sessionId = $sessionId; + } } diff --git a/src/Server.php b/src/Server.php index 6de7cba..a6012db 100644 --- a/src/Server.php +++ b/src/Server.php @@ -228,12 +228,7 @@ public function createContext(): ServerContext */ protected function handleMessage(JsonRpcRequest $request, ServerContext $context): void { - /** @var Method $methodClass */ - $methodClass = Container::getInstance()->make( - $this->methods[$request->method], - ); - - $response = $methodClass->handle($request, $context); + $response = $this->runMethodHandle($request, $context); if (! is_iterable($response)) { $this->transport->send($response->toJson()); @@ -248,6 +243,31 @@ protected function handleMessage(JsonRpcRequest $request, ServerContext $context }); } + /** + * @return iterable|JsonRpcResponse + * + * @throws JsonRpcException + */ + protected function runMethodHandle(JsonRpcRequest $request, ServerContext $context): iterable|JsonRpcResponse + { + $container = Container::getInstance(); + + /** @var Method $methodClass */ + $methodClass = $container->make( + $this->methods[$request->method], + ); + + $container->instance('mcp.request', $request->toRequest()); + + try { + $response = $methodClass->handle($request, $context); + } finally { + $container->forgetInstance('mcp.request'); + } + + return $response; + } + protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void { $response = (new Initialize)->handle($request, $context); diff --git a/src/Server/McpServiceProvider.php b/src/Server/McpServiceProvider.php index 1916a19..c9bcb1f 100644 --- a/src/Server/McpServiceProvider.php +++ b/src/Server/McpServiceProvider.php @@ -12,6 +12,7 @@ use Laravel\Mcp\Console\Commands\MakeServerCommand; use Laravel\Mcp\Console\Commands\MakeToolCommand; use Laravel\Mcp\Console\Commands\StartCommand; +use Laravel\Mcp\Request; class McpServiceProvider extends ServiceProvider { @@ -23,6 +24,7 @@ public function register(): void public function boot(): void { $this->registerRoutes(); + $this->registerContainerCallbacks(); if ($this->app->runningInConsole()) { $this->registerCommands(); @@ -63,6 +65,19 @@ protected function registerRoutes(): void Route::group([], $path); } + protected function registerContainerCallbacks(): void + { + $this->app->resolving(Request::class, function (Request $request, $app): void { + if ($app->bound('mcp.request')) { + /** @var Request $currentRequest */ + $currentRequest = $app->make('mcp.request'); + + $request->setArguments($currentRequest->all()); + $request->setSessionId($currentRequest->sessionId()); + } + }); + } + protected function registerCommands(): void { $this->commands([ diff --git a/src/Server/Methods/CallTool.php b/src/Server/Methods/CallTool.php index 6ac4d5a..e65bad3 100644 --- a/src/Server/Methods/CallTool.php +++ b/src/Server/Methods/CallTool.php @@ -28,40 +28,36 @@ class CallTool implements Errable, Method * * @throws JsonRpcException */ - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): Generator|JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse { - if (is_null($jsonRpcRequest->get('name'))) { + if (is_null($request->get('name'))) { throw new JsonRpcException( 'Missing [name] parameter.', -32602, - $jsonRpcRequest->id, + $request->id, ); } - $request = $jsonRpcRequest->toRequest(); - $tool = $context - ->tools($request) + ->tools() ->first( - fn ($tool): bool => $tool->name() === $jsonRpcRequest->params['name'], + fn ($tool): bool => $tool->name() === $request->params['name'], fn () => throw new JsonRpcException( - "Tool [{$jsonRpcRequest->params['name']}] not found.", + "Tool [{$request->params['name']}] not found.", -32602, - $jsonRpcRequest->id, + $request->id, )); try { // @phpstan-ignore-next-line - $response = Container::getInstance()->call([$tool, 'handle'], [ - 'request' => $request, - ]); + $response = Container::getInstance()->call([$tool, 'handle']); } catch (ValidationException $validationException) { $response = Response::error(ValidationMessages::from($validationException)); } return is_iterable($response) - ? $this->toJsonRpcStreamedResponse($jsonRpcRequest, $response, $this->serializable($tool)) - : $this->toJsonRpcResponse($jsonRpcRequest, $response, $this->serializable($tool)); + ? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($tool)) + : $this->toJsonRpcResponse($request, $response, $this->serializable($tool)); } /** diff --git a/src/Server/Methods/GetPrompt.php b/src/Server/Methods/GetPrompt.php index 660d534..a9b00b8 100644 --- a/src/Server/Methods/GetPrompt.php +++ b/src/Server/Methods/GetPrompt.php @@ -27,39 +27,35 @@ class GetPrompt implements Method * * @throws JsonRpcException */ - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): Generator|JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse { - if (is_null($jsonRpcRequest->get('name'))) { + if (is_null($request->get('name'))) { throw new JsonRpcException( 'Missing [name] parameter.', -32602, - $jsonRpcRequest->id, + $request->id, ); } - $request = $jsonRpcRequest->toRequest(); - - $prompt = $context->prompts($request) + $prompt = $context->prompts() ->first( - fn ($prompt): bool => $prompt->name() === $jsonRpcRequest->get('name'), + fn ($prompt): bool => $prompt->name() === $request->get('name'), fn () => throw new JsonRpcException( - "Prompt [{$jsonRpcRequest->get('name')}] not found.", + "Prompt [{$request->get('name')}] not found.", -32602, - $jsonRpcRequest->id, + $request->id, )); try { // @phpstan-ignore-next-line - $response = Container::getInstance()->call([$prompt, 'handle'], [ - 'request' => $request, - ]); + $response = Container::getInstance()->call([$prompt, 'handle']); } catch (ValidationException $validationException) { $response = Response::error('Invalid params: '.ValidationMessages::from($validationException)); } return is_iterable($response) - ? $this->toJsonRpcStreamedResponse($jsonRpcRequest, $response, $this->serializable($prompt)) - : $this->toJsonRpcResponse($jsonRpcRequest, $response, $this->serializable($prompt)); + ? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($prompt)) + : $this->toJsonRpcResponse($request, $response, $this->serializable($prompt)); } /** diff --git a/src/Server/Methods/ListPrompts.php b/src/Server/Methods/ListPrompts.php index a658609..3d95b18 100644 --- a/src/Server/Methods/ListPrompts.php +++ b/src/Server/Methods/ListPrompts.php @@ -12,14 +12,14 @@ class ListPrompts implements Method { - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse { $paginator = new CursorPaginator( - items: $context->prompts($jsonRpcRequest->toRequest()), - perPage: $context->perPage($jsonRpcRequest->get('per_page')), - cursor: $jsonRpcRequest->cursor(), + items: $context->prompts(), + perPage: $context->perPage($request->get('per_page')), + cursor: $request->cursor(), ); - return JsonRpcResponse::result($jsonRpcRequest->id, $paginator->paginate('prompts')); + return JsonRpcResponse::result($request->id, $paginator->paginate('prompts')); } } diff --git a/src/Server/Methods/ListResources.php b/src/Server/Methods/ListResources.php index 23ba93c..5259ad6 100644 --- a/src/Server/Methods/ListResources.php +++ b/src/Server/Methods/ListResources.php @@ -12,14 +12,14 @@ class ListResources implements Method { - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse { $paginator = new CursorPaginator( - items: $context->resources($jsonRpcRequest->toRequest()), - perPage: $context->perPage($jsonRpcRequest->get('per_page')), - cursor: $jsonRpcRequest->cursor(), + items: $context->resources(), + perPage: $context->perPage($request->get('per_page')), + cursor: $request->cursor(), ); - return JsonRpcResponse::result($jsonRpcRequest->id, $paginator->paginate('resources')); + return JsonRpcResponse::result($request->id, $paginator->paginate('resources')); } } diff --git a/src/Server/Methods/ListTools.php b/src/Server/Methods/ListTools.php index 6e67caa..8a3d402 100644 --- a/src/Server/Methods/ListTools.php +++ b/src/Server/Methods/ListTools.php @@ -12,16 +12,14 @@ class ListTools implements Method { - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse { - $request = $jsonRpcRequest->toRequest(); - $paginator = new CursorPaginator( - items: $context->tools($request), - perPage: $context->perPage($jsonRpcRequest->get('per_page')), - cursor: $jsonRpcRequest->cursor(), + items: $context->tools(), + perPage: $context->perPage($request->get('per_page')), + cursor: $request->cursor(), ); - return JsonRpcResponse::result($jsonRpcRequest->id, $paginator->paginate('tools')); + return JsonRpcResponse::result($request->id, $paginator->paginate('tools')); } } diff --git a/src/Server/Methods/ReadResource.php b/src/Server/Methods/ReadResource.php index 9f4edbf..50718a1 100644 --- a/src/Server/Methods/ReadResource.php +++ b/src/Server/Methods/ReadResource.php @@ -27,39 +27,35 @@ class ReadResource implements Method * * @throws JsonRpcException */ - public function handle(JsonRpcRequest $jsonRpcRequest, ServerContext $context): Generator|JsonRpcResponse + public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse { - if (is_null($jsonRpcRequest->get('uri'))) { + if (is_null($request->get('uri'))) { throw new JsonRpcException( 'Missing [uri] parameter.', -32002, - $jsonRpcRequest->id, + $request->id, ); } - $request = $jsonRpcRequest->toRequest(); - - $resource = $context->resources($request) + $resource = $context->resources() ->first( - fn (Resource $resource): bool => $resource->uri() === $jsonRpcRequest->get('uri'), + fn (Resource $resource): bool => $resource->uri() === $request->get('uri'), fn () => throw new JsonRpcException( - "Resource [{$jsonRpcRequest->get('uri')}] not found.", + "Resource [{$request->get('uri')}] not found.", -32002, - $jsonRpcRequest->id, + $request->id, )); try { // @phpstan-ignore-next-line - $response = Container::getInstance()->call([$resource, 'handle'], [ - 'request' => $request, - ]); + $response = Container::getInstance()->call([$resource, 'handle']); } catch (ValidationException $validationException) { $response = Response::error('Invalid params: '.ValidationMessages::from($validationException)); } return is_iterable($response) - ? $this->toJsonRpcStreamedResponse($jsonRpcRequest, $response, $this->serializable($resource)) - : $this->toJsonRpcResponse($jsonRpcRequest, $response, $this->serializable($resource)); + ? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($resource)) + : $this->toJsonRpcResponse($request, $response, $this->serializable($resource)); } protected function serializable(Resource $resource): callable diff --git a/src/Server/Primitive.php b/src/Server/Primitive.php index 22ec6ec..7a3adb1 100644 --- a/src/Server/Primitive.php +++ b/src/Server/Primitive.php @@ -7,7 +7,6 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Str; -use Laravel\Mcp\Request; /** * @implements Arrayable @@ -41,12 +40,10 @@ public function description(): string : $this->description; } - public function eligibleForRegistration(Request $request): bool + public function eligibleForRegistration(): bool { if (method_exists($this, 'shouldRegister')) { - return Container::getInstance()->call([$this, 'shouldRegister'], [ - 'request' => $request, - ]); + return Container::getInstance()->call([$this, 'shouldRegister']); } return true; diff --git a/src/Server/ServerContext.php b/src/Server/ServerContext.php index 8df51af..7994e03 100644 --- a/src/Server/ServerContext.php +++ b/src/Server/ServerContext.php @@ -6,7 +6,6 @@ use Illuminate\Container\Container; use Illuminate\Support\Collection; -use Laravel\Mcp\Request; class ServerContext { @@ -35,36 +34,36 @@ public function __construct( /** * @return Collection */ - public function tools(Request $request): Collection + public function tools(): Collection { return collect($this->tools)->map(fn (Tool|string $toolClass) => is_string($toolClass) ? Container::getInstance()->make($toolClass) : $toolClass - )->filter(fn (Tool $tool): bool => $tool->eligibleForRegistration($request)); + )->filter(fn (Tool $tool): bool => $tool->eligibleForRegistration()); } /** * @return Collection */ - public function resources(Request $request): Collection + public function resources(): Collection { return collect($this->resources)->map( fn (Resource|string $resourceClass) => is_string($resourceClass) ? Container::getInstance()->make($resourceClass) : $resourceClass - )->filter(fn (Resource $tool): bool => $tool->eligibleForRegistration($request)); + )->filter(fn (Resource $resource): bool => $resource->eligibleForRegistration()); } /** * @return Collection */ - public function prompts(Request $request): Collection + public function prompts(): Collection { return collect($this->prompts)->map( fn ($promptClass) => is_string($promptClass) ? Container::getInstance()->make($promptClass) : $promptClass - )->filter(fn (Prompt $prompt): bool => $prompt->eligibleForRegistration($request)); + )->filter(fn (Prompt $prompt): bool => $prompt->eligibleForRegistration()); } public function perPage(?int $requestedPerPage = null): int diff --git a/src/Server/Testing/PendingTestResponse.php b/src/Server/Testing/PendingTestResponse.php index 3981e01..e452d09 100644 --- a/src/Server/Testing/PendingTestResponse.php +++ b/src/Server/Testing/PendingTestResponse.php @@ -7,7 +7,6 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Auth\Authenticatable; use Laravel\Mcp\Server; -use Laravel\Mcp\Server\Contracts\Method; use Laravel\Mcp\Server\Exceptions\JsonRpcException; use Laravel\Mcp\Server\Primitive; use Laravel\Mcp\Server\Prompt; @@ -83,22 +82,19 @@ protected function run(string $method, Primitive|string $primitive, array $argum $server->start(); - /** @var Method $methodInstance = */ - $methodInstance = $container->make( - (fn () => $this->methods[$method])->call($server) - ); - $requestId = uniqid(); + $request = new JsonRpcRequest( + $requestId, + $method, + [ + ...$primitive->toMethodCall(), + 'arguments' => $arguments, + ], + ); + try { - $response = $methodInstance->handle(new JsonRpcRequest( - $requestId, - $method, - [ - ...$primitive->toMethodCall(), - 'arguments' => $arguments, - ], - ), $server->createContext()); + $response = (fn () => $this->runMethodHandle($request, $this->createContext()))->call($server); } catch (JsonRpcException $jsonRpcException) { $response = $jsonRpcException->toJsonRpcResponse(); } diff --git a/tests/Feature/CustomRequestTest.php b/tests/Feature/CustomRequestTest.php new file mode 100644 index 0000000..41e6dfc --- /dev/null +++ b/tests/Feature/CustomRequestTest.php @@ -0,0 +1,78 @@ +string('message')->value(); + } +} + +class LaptopShopServer extends Server +{ + protected array $prompts = [ + AskLaptop::class, + ]; + + protected array $tools = [ + BuyLaptop::class, + ]; + + protected array $resources = [ + LaptopGuidelines::class, + ]; +} + +class AskLaptop extends Prompt +{ + public function handle(MyCustomRequest $request): string + { + return $request->myMethod(); + } +} + +class BuyLaptop extends Tool +{ + public function handle(MyCustomRequest $request): string + { + return $request->myMethod(); + } +} + +class LaptopGuidelines extends Resource +{ + public function handle(MyCustomRequest $request): string + { + return $request->myMethod(); + } +} + +it('can use the custom request class on prompts', function (): void { + $response = LaptopShopServer::prompt(AskLaptop::class, [ + 'message' => 'Hello, Prompt!', + ]); + + $response->assertSee('Hello, Prompt!'); +}); + +it('can use the custom request class on tools', function (): void { + $response = LaptopShopServer::tool(BuyLaptop::class, [ + 'message' => 'Hello, Tool!', + ]); + + $response->assertSee('Hello, Tool!'); +}); + +it('can use the custom request class on resources', function (): void { + $response = LaptopShopServer::resource(LaptopGuidelines::class, [ + 'message' => 'Hello, Resource!', + ]); + + $response->assertSee('Hello, Resource!'); +}); diff --git a/tests/Unit/Methods/CallToolTest.php b/tests/Unit/Methods/CallToolTest.php index ec70a6e..d8c72c4 100644 --- a/tests/Unit/Methods/CallToolTest.php +++ b/tests/Unit/Methods/CallToolTest.php @@ -34,6 +34,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $response = $method->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); @@ -78,6 +79,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $responses = $method->handle($request, $context); [$response] = iterator_to_array($responses); @@ -126,6 +128,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $response = $method->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); @@ -168,6 +171,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $response = $method->handle($request, $context); $payload = $response->toArray(); diff --git a/tests/Unit/Methods/ListPromptsTest.php b/tests/Unit/Methods/ListPromptsTest.php index 3948cb8..1e68a25 100644 --- a/tests/Unit/Methods/ListPromptsTest.php +++ b/tests/Unit/Methods/ListPromptsTest.php @@ -152,6 +152,7 @@ public function shouldRegister(Request $request): bool $listPrompts = new ListPrompts; + $this->instance('mcp.request', $request->toRequest()); $response = $listPrompts->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); diff --git a/tests/Unit/Methods/ListResourcesTest.php b/tests/Unit/Methods/ListResourcesTest.php index ea281e6..fe4a792 100644 --- a/tests/Unit/Methods/ListResourcesTest.php +++ b/tests/Unit/Methods/ListResourcesTest.php @@ -124,6 +124,7 @@ public function shouldRegister(Request $request): bool $listResources = new ListResources; + $this->instance('mcp.request', $request->toRequest()); $response = $listResources->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); diff --git a/tests/Unit/Methods/ListToolsTest.php b/tests/Unit/Methods/ListToolsTest.php index 84b2380..027a742 100644 --- a/tests/Unit/Methods/ListToolsTest.php +++ b/tests/Unit/Methods/ListToolsTest.php @@ -339,6 +339,7 @@ public function shouldRegister(Request $request): bool $listTools = new ListTools; + $this->instance('mcp.request', $request->toRequest()); $response = $listTools->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); diff --git a/tests/Unit/Primitive/EligibleForRegistrationTest.php b/tests/Unit/Primitive/EligibleForRegistrationTest.php index b1a7ca4..b93f6e4 100644 --- a/tests/Unit/Primitive/EligibleForRegistrationTest.php +++ b/tests/Unit/Primitive/EligibleForRegistrationTest.php @@ -19,7 +19,7 @@ public function toArray(): array $request = new Request([]); - expect($primitive->eligibleForRegistration($request))->toBeTrue(); + expect($primitive->eligibleForRegistration())->toBeTrue(); }); it('calls shouldRegister with the Request and honors its return value', function (): void { @@ -47,8 +47,9 @@ public function shouldRegister(Request $request): bool $request = new Request(['foo' => 'bar']); - $result = $primitive->eligibleForRegistration($request); + $this->instance('mcp.request', $request); + $result = $primitive->eligibleForRegistration(); expect($result)->toBeFalse() - ->and($primitive->received)->toBe($request); + ->and($primitive->received->all())->toBe($request->all()); }); diff --git a/tests/Unit/Resources/CallToolTest.php b/tests/Unit/Resources/CallToolTest.php index ec70a6e..0656697 100644 --- a/tests/Unit/Resources/CallToolTest.php +++ b/tests/Unit/Resources/CallToolTest.php @@ -34,6 +34,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $response = $method->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); @@ -78,6 +79,7 @@ $method = new CallTool; + $this->instance('mcp.request', $request->toRequest()); $responses = $method->handle($request, $context); [$response] = iterator_to_array($responses); diff --git a/tests/Unit/Resources/ListPromptsTest.php b/tests/Unit/Resources/ListPromptsTest.php index 3948cb8..1e68a25 100644 --- a/tests/Unit/Resources/ListPromptsTest.php +++ b/tests/Unit/Resources/ListPromptsTest.php @@ -152,6 +152,7 @@ public function shouldRegister(Request $request): bool $listPrompts = new ListPrompts; + $this->instance('mcp.request', $request->toRequest()); $response = $listPrompts->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); diff --git a/tests/Unit/Resources/ListResourcesTest.php b/tests/Unit/Resources/ListResourcesTest.php index ea281e6..fe4a792 100644 --- a/tests/Unit/Resources/ListResourcesTest.php +++ b/tests/Unit/Resources/ListResourcesTest.php @@ -124,6 +124,7 @@ public function shouldRegister(Request $request): bool $listResources = new ListResources; + $this->instance('mcp.request', $request->toRequest()); $response = $listResources->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class); diff --git a/tests/Unit/Resources/ListToolsTest.php b/tests/Unit/Resources/ListToolsTest.php index 84b2380..027a742 100644 --- a/tests/Unit/Resources/ListToolsTest.php +++ b/tests/Unit/Resources/ListToolsTest.php @@ -339,6 +339,7 @@ public function shouldRegister(Request $request): bool $listTools = new ListTools; + $this->instance('mcp.request', $request->toRequest()); $response = $listTools->handle($request, $context); expect($response)->toBeInstanceOf(JsonRpcResponse::class);