refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Generator;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Container\Container;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Contracts\Errable;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use Laravel\Mcp\Support\ValidationMessages;
class CallTool implements Errable, Method
{
use InteractsWithResponses;
/**
* @return JsonRpcResponse|Generator<JsonRpcResponse>
*
* @throws JsonRpcException
*/
public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse
{
if (is_null($request->get('name'))) {
throw new JsonRpcException(
'Missing [name] parameter.',
-32602,
$request->id,
);
}
$tool = $context
->tools()
->first(
fn ($tool): bool => $tool->name() === $request->params['name'],
fn () => throw new JsonRpcException(
"Tool [{$request->params['name']}] not found.",
-32602,
$request->id,
));
try {
// @phpstan-ignore-next-line
$response = Container::getInstance()->call([$tool, 'handle']);
} catch (AuthenticationException|AuthorizationException $authException) {
$response = Response::error($authException->getMessage());
} catch (ValidationException $validationException) {
$response = Response::error(ValidationMessages::from($validationException));
}
return is_iterable($response)
? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($tool))
: $this->toJsonRpcResponse($request, $response, $this->serializable($tool));
}
/**
* @return callable(ResponseFactory): array<string, mixed>
*/
protected function serializable(Tool $tool): callable
{
return fn (ResponseFactory $factory): array => $factory->mergeStructuredContent(
$factory->mergeMeta([
'content' => $factory->responses()->map(fn (Response $response): array => $response->content()->toTool($tool))->all(),
'isError' => $factory->responses()->contains(fn (Response $response): bool => $response->isError()),
])
);
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Illuminate\Container\Container;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Completions\CompletionResponse;
use Laravel\Mcp\Server\Contracts\Completable;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\ResolvesPrompts;
use Laravel\Mcp\Server\Methods\Concerns\ResolvesResources;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class CompletionComplete implements Method
{
use ResolvesPrompts;
use ResolvesResources;
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
if (! $context->hasCapability(Server::CAPABILITY_COMPLETIONS)) {
throw new JsonRpcException(
'Server does not support completions capability.',
-32601,
$request->id,
);
}
$ref = $request->get('ref');
$argument = $request->get('argument');
if (is_null($ref) || is_null($argument)) {
throw new JsonRpcException(
'Missing required parameters: ref and argument',
-32602,
$request->id,
);
}
try {
$primitive = $this->resolvePrimitive($ref, $context);
} catch (InvalidArgumentException $invalidArgumentException) {
throw new JsonRpcException($invalidArgumentException->getMessage(), -32602, $request->id);
}
if (! $primitive instanceof Completable) {
$result = CompletionResponse::empty();
return JsonRpcResponse::result($request->id, [
'completion' => $result->toArray(),
]);
}
$argumentName = Arr::get($argument, 'name');
$argumentValue = Arr::get($argument, 'value', '');
if (is_null($argumentName)) {
throw new JsonRpcException(
'Missing argument name.',
-32602,
$request->id,
);
}
$contextArguments = Arr::get($request->get('context'), 'arguments', []);
$result = $this->invokeCompletion($primitive, $argumentName, $argumentValue, $contextArguments);
return JsonRpcResponse::result($request->id, [
'completion' => $result->toArray(),
]);
}
/**
* @param array<string, mixed> $ref
*/
protected function resolvePrimitive(array $ref, ServerContext $context): Prompt|Resource|HasUriTemplate
{
return match (Arr::get($ref, 'type')) {
'ref/prompt' => $this->resolvePrompt(Arr::get($ref, 'name'), $context),
'ref/resource' => $this->resolveResource(Arr::get($ref, 'uri'), $context),
default => throw new InvalidArgumentException('Invalid reference type. Expected ref/prompt or ref/resource.'),
};
}
/**
* @param array<string, mixed> $context
*/
protected function invokeCompletion(
Completable $primitive,
string $argumentName,
string $argumentValue,
array $context
): mixed {
$container = Container::getInstance();
$result = $container->call($primitive->complete(...), [
'argument' => $argumentName,
'value' => $argumentValue,
'context' => $context,
]);
return $result->resolve($argumentValue);
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods\Concerns;
use Generator;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Content\Notification;
use Laravel\Mcp\Server\Contracts\Errable;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
trait InteractsWithResponses
{
/**
* @param array<int, Response|ResponseFactory|string>|Response|ResponseFactory|string $response
*
* @throws JsonRpcException
*/
protected function toJsonRpcResponse(JsonRpcRequest $request, Response|ResponseFactory|array|string $response, callable $serializable): JsonRpcResponse
{
$responseFactory = $this->toResponseFactory($response);
$responseFactory->responses()->each(function (Response $response) use ($request): void {
if (! $this instanceof Errable && $response->isError()) {
throw new JsonRpcException(
$response->content()->__toString(), // @phpstan-ignore-line
-32603,
$request->id,
);
}
});
return JsonRpcResponse::result($request->id, $serializable($responseFactory));
}
/**
* @param iterable<Response|ResponseFactory|string> $responses
* @return Generator<JsonRpcResponse>
*/
protected function toJsonRpcStreamedResponse(JsonRpcRequest $request, iterable $responses, callable $serializable): Generator
{
/** @var array<int, Response|ResponseFactory|string> $pendingResponses */
$pendingResponses = [];
try {
foreach ($responses as $response) {
if ($response instanceof Response && $response->isNotification()) {
/** @var Notification $content */
$content = $response->content();
yield JsonRpcResponse::notification(
...$content->toArray(),
);
continue;
}
$pendingResponses[] = $response;
}
} catch (AuthenticationException|AuthorizationException $authException) {
yield $this->toJsonRpcResponse(
$request,
Response::error($authException->getMessage()),
$serializable,
);
return;
} catch (ValidationException $validationException) {
yield $this->toJsonRpcResponse(
$request,
Response::error($validationException->getMessage()),
$serializable,
);
return;
}
yield $this->toJsonRpcResponse($request, $pendingResponses, $serializable);
}
protected function isBinary(string $content): bool
{
return str_contains($content, "\0");
}
/**
* @param array<int, Response|ResponseFactory|string>|Response|ResponseFactory|string $response
*/
private function toResponseFactory(Response|ResponseFactory|array|string $response): ResponseFactory
{
$responseFactory = is_array($response) && count($response) === 1
? Arr::first($response)
: $response;
if ($responseFactory instanceof ResponseFactory) {
return $responseFactory;
}
$items = is_array($responseFactory) ? $responseFactory : [$responseFactory];
$responses = collect($items)
->map(function ($item): Response {
if ($item instanceof Response) {
return $item;
}
if (! is_string($item)) {
throw new InvalidArgumentException('Response must be a Response instance or string');
}
return $this->isBinary($item)
? Response::blob($item)
: Response::text($item);
});
return new ResponseFactory($responses->all());
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods\Concerns;
use InvalidArgumentException;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\ServerContext;
trait ResolvesPrompts
{
protected function resolvePrompt(?string $name, ServerContext $context): Prompt
{
if (! $name) {
throw new InvalidArgumentException('Missing [name] parameter.');
}
return $context->prompts()->first(
fn ($prompt): bool => $prompt->name() === $name,
fn () => throw new InvalidArgumentException("Prompt [{$name}] not found.")
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods\Concerns;
use InvalidArgumentException;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ServerContext;
trait ResolvesResources
{
protected function resolveResource(?string $uri, ServerContext $context): Resource
{
if (! $uri) {
throw new InvalidArgumentException('Missing [uri] parameter.');
}
$resource = $context->resources()->first(fn ($resource): bool => $resource->uri() === $uri)
?? $context->resourceTemplates()->first(fn ($template): bool => (string) $template->uriTemplate() === $uri
|| $template->uriTemplate()->match($uri) !== null);
if (! $resource) {
throw new InvalidArgumentException("Resource [{$uri}] not found.");
}
return $resource;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Generator;
use Illuminate\Container\Container;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
use Laravel\Mcp\Server\Methods\Concerns\ResolvesPrompts;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use Laravel\Mcp\Support\ValidationMessages;
class GetPrompt implements Method
{
use InteractsWithResponses;
use ResolvesPrompts;
/**
* @return Generator<JsonRpcResponse>|JsonRpcResponse
*/
public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse
{
try {
$prompt = $this->resolvePrompt($request->get('name'), $context);
} catch (InvalidArgumentException $invalidArgumentException) {
throw new JsonRpcException($invalidArgumentException->getMessage(), -32602, $request->id);
}
try {
// @phpstan-ignore-next-line
$response = Container::getInstance()->call([$prompt, 'handle']);
} catch (ValidationException $validationException) {
$response = Response::error('Invalid params: '.ValidationMessages::from($validationException));
}
return is_iterable($response)
? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($prompt))
: $this->toJsonRpcResponse($request, $response, $this->serializable($prompt));
}
/**
* @return callable(ResponseFactory): array<string, mixed>
*/
protected function serializable(Prompt $prompt): callable
{
return fn (ResponseFactory $factory): array => $factory->mergeMeta([
'description' => $prompt->description(),
'messages' => $factory->responses()->map(fn (Response $response): array => [
'role' => $response->role()->value,
'content' => $response->content()->toPrompt($prompt),
])->all(),
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class Initialize implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$requestedVersion = $request->params['protocolVersion'] ?? null;
if (! is_null($requestedVersion) && ! in_array($requestedVersion, $context->supportedProtocolVersions, true)) {
throw new JsonRpcException(
message: 'Unsupported protocol version',
code: -32602,
requestId: $request->id,
data: [
'supported' => $context->supportedProtocolVersions,
'requested' => $requestedVersion,
]
);
}
$protocolVersion = $requestedVersion ?? $context->supportedProtocolVersions[0];
$initResult = [
'protocolVersion' => $protocolVersion,
'capabilities' => $context->serverCapabilities,
'serverInfo' => [
'name' => $context->serverName,
'version' => $context->serverVersion,
],
'instructions' => $context->instructions,
];
if (in_array($protocolVersion, ['2024-11-05', '2025-03-26'], true)) {
unset($initResult['instructions']);
}
return JsonRpcResponse::result($request->id, $initResult);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Pagination\CursorPaginator;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class ListPrompts implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$paginator = new CursorPaginator(
items: $context->prompts(),
perPage: $context->perPage($request->get('per_page')),
cursor: $request->cursor(),
);
return JsonRpcResponse::result($request->id, $paginator->paginate('prompts'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Pagination\CursorPaginator;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class ListResourceTemplates implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$paginator = new CursorPaginator(
items: $context->resourceTemplates(),
perPage: $context->perPage($request->get('per_page')),
cursor: $request->cursor(),
);
return JsonRpcResponse::result($request->id, $paginator->paginate('resourceTemplates'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Pagination\CursorPaginator;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class ListResources implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$paginator = new CursorPaginator(
items: $context->resources(),
perPage: $context->perPage($request->get('per_page')),
cursor: $request->cursor(),
);
return JsonRpcResponse::result($request->id, $paginator->paginate('resources'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Pagination\CursorPaginator;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class ListTools implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
$paginator = new CursorPaginator(
items: $context->tools(),
perPage: $context->perPage($request->get('per_page')),
cursor: $request->cursor(),
);
return JsonRpcResponse::result($request->id, $paginator->paginate('tools'));
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
class Ping implements Method
{
public function handle(JsonRpcRequest $request, ServerContext $context): JsonRpcResponse
{
return JsonRpcResponse::result($request->id, []);
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Laravel\Mcp\Server\Methods;
use Generator;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\AppResource;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Contracts\Method;
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
use Laravel\Mcp\Server\Methods\Concerns\InteractsWithResponses;
use Laravel\Mcp\Server\Methods\Concerns\ResolvesResources;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Server\ServerContext;
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
use Laravel\Mcp\Support\ValidationMessages;
class ReadResource implements Method
{
use InteractsWithResponses;
use ResolvesResources;
/**
* @return Generator<JsonRpcResponse>|JsonRpcResponse
*
* @throws BindingResolutionException
*/
public function handle(JsonRpcRequest $request, ServerContext $context): Generator|JsonRpcResponse
{
$uri = $request->get('uri');
try {
$resource = $this->resolveResource($uri, $context);
} catch (InvalidArgumentException $invalidArgumentException) {
throw new JsonRpcException($invalidArgumentException->getMessage(), -32002, $request->id);
}
try {
$response = $this->invokeResource($resource, $uri);
} catch (ValidationException $validationException) {
$response = Response::error('Invalid params: '.ValidationMessages::from($validationException));
}
return is_iterable($response)
? $this->toJsonRpcStreamedResponse($request, $response, $this->serializable($resource, $uri))
: $this->toJsonRpcResponse($request, $response, $this->serializable($resource, $uri));
}
/**
* @throws BindingResolutionException
* @throws ValidationException
*/
protected function invokeResource(Resource $resource, string $uri): mixed
{
$container = Container::getInstance();
$request = $container->make(Request::class);
$request->setUri($uri);
if ($resource instanceof HasUriTemplate) {
$variables = $resource->uriTemplate()->match($uri) ?? [];
$request->merge($variables);
}
$container->instance(Request::class, $request);
if ($resource instanceof AppResource) {
$container->instance('mcp.library_scripts', $resource->libraryScripts());
}
try {
// @phpstan-ignore-next-line
return $container->call([$resource, 'handle']);
} finally {
$container->forgetInstance(Request::class);
$container->forgetInstance('mcp.library_scripts');
}
}
protected function serializable(Resource $resource, string $uri): callable
{
$appMeta = $resource instanceof AppResource ? $resource->resolvedAppMeta() : null;
return fn (ResponseFactory $factory): array => $factory->mergeMeta([
'contents' => $factory->responses()->map(function (Response $response) use ($resource, $uri, $appMeta): array {
$content = [
...$response->content()->toResource($resource),
'uri' => $uri,
];
if ($appMeta !== null && $appMeta !== []) {
$content['_meta'] = array_merge($content['_meta'] ?? [], [
'ui' => $appMeta,
]);
}
return $content;
})->all(),
]);
}
}