refactor: susun semula struktur folder — Laravel source ke src/
This commit is contained in:
164
vendor/laravel/mcp/src/Console/Commands/InspectorCommand.php
vendored
Normal file
164
vendor/laravel/mcp/src/Console/Commands/InspectorCommand.php
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Mcp\Server\Registrar;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'mcp:inspector',
|
||||
description: 'Open the MCP Inspector tool to debug and test MCP Servers'
|
||||
)]
|
||||
class InspectorCommand extends Command
|
||||
{
|
||||
public function handle(Registrar $registrar): int
|
||||
{
|
||||
$handle = $this->argument('handle');
|
||||
|
||||
if (! is_string($handle)) {
|
||||
$this->components->error('Please pass a valid MCP server handle');
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$this->components->info("Starting the MCP Inspector for server [{$handle}]");
|
||||
|
||||
$localServer = $registrar->getLocalServer($handle);
|
||||
$route = $registrar->getWebServer($handle);
|
||||
|
||||
$servers = $registrar->servers();
|
||||
if ($servers === []) {
|
||||
$this->components->error('No MCP servers found. Please run `php artisan make:mcp-server [name]`');
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
// Only one server, we should just run it for them
|
||||
if (count($servers) === 1) {
|
||||
$server = array_shift($servers);
|
||||
[$localServer, $route] = match (true) {
|
||||
is_callable($server) => [$server, null],
|
||||
$server::class === Route::class => [null, $server],
|
||||
default => [null, null],
|
||||
};
|
||||
}
|
||||
|
||||
if (is_null($localServer) && is_null($route)) {
|
||||
$availableServers = Arr::map(array_keys($servers), fn ($server): string => "[{$server}]");
|
||||
$this->components->error('MCP Server with name ['.$handle.'] not found. Available servers: '.Arr::join($availableServers, ', '));
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$env = [];
|
||||
|
||||
if (is_string($host = $this->option('host'))) {
|
||||
$env['HOST'] = $host;
|
||||
}
|
||||
|
||||
if (is_string($port = $this->option('port'))) {
|
||||
$env['CLIENT_PORT'] = $port;
|
||||
}
|
||||
|
||||
if ($localServer !== null) {
|
||||
$artisanPath = base_path('artisan');
|
||||
|
||||
$command = [
|
||||
'npx',
|
||||
'@modelcontextprotocol/inspector',
|
||||
'--transport',
|
||||
'stdio',
|
||||
$this->phpBinary(),
|
||||
$artisanPath,
|
||||
"mcp:start {$handle}",
|
||||
];
|
||||
|
||||
$guidance = [
|
||||
'Transport Type' => 'STDIO',
|
||||
'Command' => $this->phpBinary(),
|
||||
'Arguments' => implode(' ', [
|
||||
str_replace('\\', '/', $artisanPath),
|
||||
'mcp:start',
|
||||
$handle,
|
||||
]),
|
||||
];
|
||||
} else {
|
||||
$serverUrl = url($route->uri());
|
||||
if (parse_url($serverUrl, PHP_URL_SCHEME) === 'https') {
|
||||
$env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
||||
}
|
||||
|
||||
$command = [
|
||||
'npx',
|
||||
'@modelcontextprotocol/inspector',
|
||||
'--transport',
|
||||
'http',
|
||||
'--server-url',
|
||||
$serverUrl,
|
||||
];
|
||||
|
||||
$guidance = [
|
||||
'Transport Type' => 'Streamable HTTP',
|
||||
'URL' => $serverUrl,
|
||||
'Secure' => 'Your project must be accessible on HTTP for this to work due to how node manages SSL trust',
|
||||
];
|
||||
}
|
||||
|
||||
$process = new Process($command, null, $env);
|
||||
$process->setTimeout(null);
|
||||
|
||||
try {
|
||||
foreach ($guidance as $guidanceKey => $guidanceValue) {
|
||||
$this->info(sprintf('%s => %s', $guidanceKey, $guidanceValue));
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
|
||||
$process->mustRun(function (int|string $type, string $buffer): void {
|
||||
echo $buffer;
|
||||
});
|
||||
} catch (Exception $exception) {
|
||||
$this->components->error('Failed to start MCP Inspector: '.$exception->getMessage());
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getArguments(): array
|
||||
{
|
||||
return [
|
||||
['handle', InputArgument::REQUIRED, 'The handle or route of the MCP server to inspect.'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int|null>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['host', null, InputOption::VALUE_OPTIONAL, 'The host the inspector should bind to'],
|
||||
['port', null, InputOption::VALUE_OPTIONAL, 'The port the inspector should bind to'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function phpBinary(): string
|
||||
{
|
||||
return (new PhpExecutableFinder)->find(false) ?: 'php';
|
||||
}
|
||||
}
|
||||
108
vendor/laravel/mcp/src/Console/Commands/MakeAppResourceCommand.php
vendored
Normal file
108
vendor/laravel/mcp/src/Console/Commands/MakeAppResourceCommand.php
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'make:mcp-app-resource',
|
||||
description: 'Create a new MCP app resource class and linked view'
|
||||
)]
|
||||
class MakeAppResourceCommand extends GeneratorCommand
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'AppResource';
|
||||
|
||||
public function handle(): ?bool
|
||||
{
|
||||
$result = parent::handle();
|
||||
|
||||
if ($result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->createBladeView();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function buildClass($name): string
|
||||
{
|
||||
$viewName = collect(explode('/', $this->getKebabName()))
|
||||
->implode('.');
|
||||
|
||||
return str_replace(
|
||||
'{{ view }}',
|
||||
'mcp.'.$viewName,
|
||||
parent::buildClass($name),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getStub(): string
|
||||
{
|
||||
return $this->resolveStub('mcp-app-resource.stub');
|
||||
}
|
||||
|
||||
protected function getDefaultNamespace($rootNamespace): string
|
||||
{
|
||||
return "{$rootNamespace}\\Mcp\\Resources";
|
||||
}
|
||||
|
||||
protected function createBladeView(): void
|
||||
{
|
||||
$viewPath = $this->getViewPath();
|
||||
|
||||
if ($this->files->exists($viewPath) && ! $this->option('force')) {
|
||||
$this->components->warn("View [{$viewPath}] already exists.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($viewPath));
|
||||
|
||||
$this->files->put($viewPath, $this->files->get($this->getViewStub()));
|
||||
|
||||
$this->components->info("View [{$viewPath}] created successfully.");
|
||||
}
|
||||
|
||||
protected function getViewStub(): string
|
||||
{
|
||||
return $this->resolveStub('mcp-app-resource.view.stub');
|
||||
}
|
||||
|
||||
protected function resolveStub(string $name): string
|
||||
{
|
||||
return file_exists($customPath = $this->laravel->basePath("stubs/{$name}"))
|
||||
? $customPath
|
||||
: __DIR__."/../../../stubs/{$name}";
|
||||
}
|
||||
|
||||
protected function getViewPath(): string
|
||||
{
|
||||
return resource_path('views/mcp/'.$this->getKebabName().'.blade.php');
|
||||
}
|
||||
|
||||
protected function getKebabName(): string
|
||||
{
|
||||
return collect(explode('/', $this->getNameInput()))
|
||||
->map(fn (string $segment) => Str::kebab($segment))
|
||||
->implode('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'],
|
||||
];
|
||||
}
|
||||
}
|
||||
43
vendor/laravel/mcp/src/Console/Commands/MakePromptCommand.php
vendored
Normal file
43
vendor/laravel/mcp/src/Console/Commands/MakePromptCommand.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'make:mcp-prompt',
|
||||
description: 'Create a new MCP prompt class'
|
||||
)]
|
||||
class MakePromptCommand extends GeneratorCommand
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Prompt';
|
||||
|
||||
protected function getStub(): string
|
||||
{
|
||||
return file_exists($customPath = $this->laravel->basePath('stubs/mcp-prompt.stub'))
|
||||
? $customPath
|
||||
: __DIR__.'/../../../stubs/mcp-prompt.stub';
|
||||
}
|
||||
|
||||
protected function getDefaultNamespace($rootNamespace): string
|
||||
{
|
||||
return "{$rootNamespace}\\Mcp\\Prompts";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the prompt already exists'],
|
||||
];
|
||||
}
|
||||
}
|
||||
43
vendor/laravel/mcp/src/Console/Commands/MakeResourceCommand.php
vendored
Normal file
43
vendor/laravel/mcp/src/Console/Commands/MakeResourceCommand.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'make:mcp-resource',
|
||||
description: 'Create a new MCP resource class'
|
||||
)]
|
||||
class MakeResourceCommand extends GeneratorCommand
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Resource';
|
||||
|
||||
protected function getStub(): string
|
||||
{
|
||||
return file_exists($customPath = $this->laravel->basePath('stubs/mcp-resource.stub'))
|
||||
? $customPath
|
||||
: __DIR__.'/../../../stubs/mcp-resource.stub';
|
||||
}
|
||||
|
||||
protected function getDefaultNamespace($rootNamespace): string
|
||||
{
|
||||
return "{$rootNamespace}\\Mcp\\Resources";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the resource already exists'],
|
||||
];
|
||||
}
|
||||
}
|
||||
63
vendor/laravel/mcp/src/Console/Commands/MakeServerCommand.php
vendored
Normal file
63
vendor/laravel/mcp/src/Console/Commands/MakeServerCommand.php
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'make:mcp-server',
|
||||
description: 'Create a new MCP server class'
|
||||
)]
|
||||
class MakeServerCommand extends GeneratorCommand
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Server';
|
||||
|
||||
protected function getStub(): string
|
||||
{
|
||||
return file_exists($customPath = $this->laravel->basePath('stubs/mcp-server.stub'))
|
||||
? $customPath
|
||||
: __DIR__.'/../../../stubs/mcp-server.stub';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $rootNamespace
|
||||
*/
|
||||
protected function getDefaultNamespace($rootNamespace): string
|
||||
{
|
||||
return "{$rootNamespace}\\Mcp\\Servers";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the server already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
protected function buildClass($name): string
|
||||
{
|
||||
$stub = parent::buildClass($name);
|
||||
|
||||
$className = class_basename($name);
|
||||
|
||||
$serverDisplayName = trim((string) preg_replace('/(?<!^)([A-Z])/', ' $1', $className));
|
||||
|
||||
return str_replace('{{ serverDisplayName }}', $serverDisplayName, $stub);
|
||||
}
|
||||
}
|
||||
67
vendor/laravel/mcp/src/Console/Commands/MakeToolCommand.php
vendored
Normal file
67
vendor/laravel/mcp/src/Console/Commands/MakeToolCommand.php
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\GeneratorCommand;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'make:mcp-tool',
|
||||
description: 'Create a new MCP tool class'
|
||||
)]
|
||||
class MakeToolCommand extends GeneratorCommand
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Tool';
|
||||
|
||||
protected function getStub(): string
|
||||
{
|
||||
return file_exists($customPath = $this->laravel->basePath('stubs/mcp-tool.stub'))
|
||||
? $customPath
|
||||
: __DIR__.'/../../../stubs/mcp-tool.stub';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $rootNamespace
|
||||
*/
|
||||
protected function getDefaultNamespace($rootNamespace): string
|
||||
{
|
||||
return "{$rootNamespace}\\Mcp\\Tools";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the tool already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
protected function buildClass($name): string
|
||||
{
|
||||
$stub = parent::buildClass($name);
|
||||
|
||||
$className = class_basename($name);
|
||||
$title = Str::headline($className);
|
||||
|
||||
return str_replace(
|
||||
'{{ title }}',
|
||||
$title,
|
||||
$stub,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
vendor/laravel/mcp/src/Console/Commands/StartCommand.php
vendored
Normal file
46
vendor/laravel/mcp/src/Console/Commands/StartCommand.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Laravel\Mcp\Server\Registrar;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'mcp:start',
|
||||
description: 'Start the MCP Server for a given handle'
|
||||
)]
|
||||
class StartCommand extends Command
|
||||
{
|
||||
public function handle(Registrar $registrar): int
|
||||
{
|
||||
$handle = $this->argument('handle');
|
||||
|
||||
assert(is_string($handle));
|
||||
|
||||
$server = $registrar->getLocalServer($handle);
|
||||
|
||||
if ($server === null) {
|
||||
$this->components->error("MCP Server with name [{$handle}] not found. Did you register it using [Mcp::local()]?");
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$server();
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string|int>>
|
||||
*/
|
||||
protected function getArguments(): array
|
||||
{
|
||||
return [
|
||||
['handle', InputArgument::REQUIRED, 'The handle of the MCP server to start.'],
|
||||
];
|
||||
}
|
||||
}
|
||||
11
vendor/laravel/mcp/src/Enums/Role.php
vendored
Normal file
11
vendor/laravel/mcp/src/Enums/Role.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Enums;
|
||||
|
||||
enum Role: string
|
||||
{
|
||||
case Assistant = 'assistant';
|
||||
case User = 'user';
|
||||
}
|
||||
47
vendor/laravel/mcp/src/Events/SessionInitialized.php
vendored
Normal file
47
vendor/laravel/mcp/src/Events/SessionInitialized.php
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Events;
|
||||
|
||||
class SessionInitialized
|
||||
{
|
||||
/**
|
||||
* @param array{name?: string, title?: string, version?: string}|null $clientInfo
|
||||
* @param array<string, mixed>|null $clientCapabilities
|
||||
*
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $sessionId,
|
||||
public readonly ?array $clientInfo,
|
||||
public readonly ?string $protocolVersion,
|
||||
public readonly ?array $clientCapabilities,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client name from clientInfo, if available.
|
||||
*/
|
||||
public function clientName(): ?string
|
||||
{
|
||||
return $this->clientInfo['name'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client title from clientInfo, if available.
|
||||
*/
|
||||
public function clientTitle(): ?string
|
||||
{
|
||||
return $this->clientInfo['title'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client version from clientInfo, if available.
|
||||
*/
|
||||
public function clientVersion(): ?string
|
||||
{
|
||||
return $this->clientInfo['version'] ?? null;
|
||||
}
|
||||
}
|
||||
15
vendor/laravel/mcp/src/Exceptions/NotImplementedException.php
vendored
Normal file
15
vendor/laravel/mcp/src/Exceptions/NotImplementedException.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotImplementedException extends Exception
|
||||
{
|
||||
public static function forMethod(string $class, string $method): static
|
||||
{
|
||||
return new static("The method [{$class}@{$method}] is not implemented yet.");
|
||||
}
|
||||
}
|
||||
30
vendor/laravel/mcp/src/Facades/Mcp.php
vendored
Normal file
30
vendor/laravel/mcp/src/Facades/Mcp.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Laravel\Mcp\Server\Registrar;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Routing\Route web(string $route, string $serverClass)
|
||||
* @method static void local(string $handle, string $serverClass)
|
||||
* @method static callable|null getLocalServer(string $handle)
|
||||
* @method static \Illuminate\Routing\Route|null getWebServer(string $route)
|
||||
* @method static array servers()
|
||||
* @method static void oauthRoutes(string $oauthPrefix = 'oauth')
|
||||
* @method static array ensureMcpScope()
|
||||
*
|
||||
* @see \Laravel\Mcp\Server\Registrar
|
||||
*/
|
||||
class Mcp extends Facade
|
||||
{
|
||||
/**
|
||||
* @return class-string<Registrar>
|
||||
*/
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return Registrar::class;
|
||||
}
|
||||
}
|
||||
146
vendor/laravel/mcp/src/Request.php
vendored
Normal file
146
vendor/laravel/mcp/src/Request.php
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use Illuminate\Support\Traits\InteractsWithData;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class Request implements Arrayable
|
||||
{
|
||||
use Conditionable;
|
||||
use InteractsWithData;
|
||||
use Macroable;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
* @param array<string, mixed>|null $meta
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $arguments = [],
|
||||
protected ?string $sessionId = null,
|
||||
protected ?array $meta = null,
|
||||
protected ?string $uri = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string>|array-key|null $keys
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function all(mixed $keys = null): array
|
||||
{
|
||||
if (is_null($keys)) {
|
||||
return $this->data();
|
||||
}
|
||||
|
||||
return array_intersect_key($this->data(), array_flip(is_array($keys) ? $keys : func_get_args()));
|
||||
}
|
||||
|
||||
protected function data(mixed $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if (is_null($key)) {
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
return $this->arguments[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->data($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
*/
|
||||
public function merge(array $data): static
|
||||
{
|
||||
$this->arguments = array_merge($this->arguments, $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $rules
|
||||
* @param array<string, mixed> $messages
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(array $rules, array $messages = [], array $attributes = []): array
|
||||
{
|
||||
return Validator::validate($this->all(), $rules, $messages, $attributes);
|
||||
}
|
||||
|
||||
public function user(?string $guard = null): ?Authenticatable
|
||||
{
|
||||
$auth = Container::getInstance()->make('auth');
|
||||
|
||||
return call_user_func($auth->userResolver(), $guard);
|
||||
}
|
||||
|
||||
public function sessionId(): ?string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function meta(): ?array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function uri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public function setArguments(array $arguments): void
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function setSessionId(?string $sessionId): void
|
||||
{
|
||||
$this->sessionId = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $meta
|
||||
*/
|
||||
public function setMeta(?array $meta): void
|
||||
{
|
||||
$this->meta = $meta;
|
||||
}
|
||||
|
||||
public function setUri(?string $uri): void
|
||||
{
|
||||
$this->uri = $uri;
|
||||
}
|
||||
}
|
||||
186
vendor/laravel/mcp/src/Response.php
vendored
Normal file
186
vendor/laravel/mcp/src/Response.php
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp;
|
||||
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
use Laravel\Mcp\Enums\Role;
|
||||
use Laravel\Mcp\Server\Content\Audio;
|
||||
use Laravel\Mcp\Server\Content\Blob;
|
||||
use Laravel\Mcp\Server\Content\Image;
|
||||
use Laravel\Mcp\Server\Content\Notification;
|
||||
use Laravel\Mcp\Server\Content\Text;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
|
||||
class Response
|
||||
{
|
||||
use Conditionable;
|
||||
use Macroable;
|
||||
|
||||
protected function __construct(
|
||||
protected Content $content,
|
||||
protected Role $role = Role::User,
|
||||
protected bool $isError = false,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function notification(string $method, array $params = []): static
|
||||
{
|
||||
return new static(new Notification($method, $params));
|
||||
}
|
||||
|
||||
public static function text(string $text): static
|
||||
{
|
||||
return new static(new Text($text));
|
||||
}
|
||||
|
||||
public static function html(string $path): static
|
||||
{
|
||||
$path = str_starts_with($path, '/') || preg_match('/^[a-zA-Z]:[\\\\\\/]/', $path) ? $path : resource_path($path);
|
||||
|
||||
if (! file_exists($path)) {
|
||||
throw new InvalidArgumentException("File not found at path [{$path}].");
|
||||
}
|
||||
|
||||
return static::text((string) file_get_contents($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $mergeData
|
||||
*/
|
||||
public static function view(string $view, array $data = [], array $mergeData = []): static
|
||||
{
|
||||
return static::text(view($view, $data, $mergeData)->render());
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @throws JsonException
|
||||
*/
|
||||
public static function json(mixed $content): static
|
||||
{
|
||||
return static::text(json_encode($content, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
public static function blob(string $content): static
|
||||
{
|
||||
return new static(new Blob($content));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $response
|
||||
*/
|
||||
public static function structured(array $response): ResponseFactory
|
||||
{
|
||||
if ($response === []) {
|
||||
throw new InvalidArgumentException('Structured content cannot be empty.');
|
||||
}
|
||||
|
||||
try {
|
||||
$json = json_encode($response, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} catch (JsonException $jsonException) {
|
||||
throw new InvalidArgumentException("Invalid structured content: {$jsonException->getMessage()}", 0, $jsonException);
|
||||
}
|
||||
|
||||
$content = Response::text($json);
|
||||
|
||||
return (new ResponseFactory($content))->withStructuredContent($response);
|
||||
}
|
||||
|
||||
public static function error(string $text): static
|
||||
{
|
||||
return new static(new Text($text), isError: true);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response|array<int, Response> $responses
|
||||
*/
|
||||
public static function make(Response|array $responses): ResponseFactory
|
||||
{
|
||||
return new ResponseFactory($responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string $meta
|
||||
*/
|
||||
public function withMeta(array|string $meta, mixed $value = null): static
|
||||
{
|
||||
$this->content->setMeta($meta, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function audio(string $data, string $mimeType = 'audio/wav'): static
|
||||
{
|
||||
return new static(new Audio($data, $mimeType));
|
||||
}
|
||||
|
||||
public static function image(string $data, string $mimeType = 'image/png'): static
|
||||
{
|
||||
return new static(new Image($data, $mimeType));
|
||||
}
|
||||
|
||||
public static function fromStorage(string $path, ?string $disk = null, ?string $mimeType = null): static
|
||||
{
|
||||
/** @var FilesystemAdapter $storage */
|
||||
$storage = Storage::disk($disk);
|
||||
|
||||
try {
|
||||
$data = $storage->get($path);
|
||||
} catch (UnableToReadFile $unableToReadFile) {
|
||||
throw new InvalidArgumentException("File not found at path [{$path}].", 0, $unableToReadFile);
|
||||
}
|
||||
|
||||
if ($data === null) {
|
||||
throw new InvalidArgumentException("File not found at path [{$path}].");
|
||||
}
|
||||
|
||||
$mimeType ??= $storage->mimeType($path) ?: throw new InvalidArgumentException(
|
||||
"Unable to determine MIME type for [{$path}].",
|
||||
);
|
||||
|
||||
return match (true) {
|
||||
str_starts_with($mimeType, 'image/') => static::image($data, $mimeType),
|
||||
str_starts_with($mimeType, 'audio/') => static::audio($data, $mimeType),
|
||||
default => throw new InvalidArgumentException("Unsupported MIME type [{$mimeType}] for [{$path}]."),
|
||||
};
|
||||
}
|
||||
|
||||
public function asAssistant(): static
|
||||
{
|
||||
return new static($this->content, Role::Assistant, $this->isError);
|
||||
}
|
||||
|
||||
public function isNotification(): bool
|
||||
{
|
||||
return $this->content instanceof Notification;
|
||||
}
|
||||
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->isError;
|
||||
}
|
||||
|
||||
public function role(): Role
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
}
|
||||
88
vendor/laravel/mcp/src/ResponseFactory.php
vendored
Normal file
88
vendor/laravel/mcp/src/ResponseFactory.php
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Concerns\HasStructuredContent;
|
||||
|
||||
class ResponseFactory
|
||||
{
|
||||
use Conditionable;
|
||||
use HasMeta;
|
||||
use HasStructuredContent;
|
||||
use Macroable;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Response>
|
||||
*/
|
||||
protected Collection $responses;
|
||||
|
||||
/**
|
||||
* @param Response|array<int, Response> $responses
|
||||
*/
|
||||
public function __construct(Response|array $responses)
|
||||
{
|
||||
$wrapped = Arr::wrap($responses);
|
||||
|
||||
foreach ($wrapped as $index => $response) {
|
||||
if (! $response instanceof Response) {
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid response type at index {$index}: Expected ".Response::class.', but received '.get_debug_type($response).'.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->responses = collect($wrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<string, mixed> $meta
|
||||
*/
|
||||
public function withMeta(string|array $meta, mixed $value = null): static
|
||||
{
|
||||
$this->setMeta($meta, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $structuredContent
|
||||
*/
|
||||
public function withStructuredContent(array $structuredContent): static
|
||||
{
|
||||
$this->setStructuredContent($structuredContent);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Response>
|
||||
*/
|
||||
public function responses(): Collection
|
||||
{
|
||||
return $this->responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getMeta(): ?array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getStructuredContent(): ?array
|
||||
{
|
||||
return $this->structuredContent;
|
||||
}
|
||||
}
|
||||
347
vendor/laravel/mcp/src/Server.php
vendored
Normal file
347
vendor/laravel/mcp/src/Server.php
vendored
Normal file
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Events\SessionInitialized;
|
||||
use Laravel\Mcp\Server\AppResource;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
use Laravel\Mcp\Server\Concerns\ReadsAttributes;
|
||||
use Laravel\Mcp\Server\Contracts\Method;
|
||||
use Laravel\Mcp\Server\Contracts\Transport;
|
||||
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
|
||||
use Laravel\Mcp\Server\Methods\CallTool;
|
||||
use Laravel\Mcp\Server\Methods\CompletionComplete;
|
||||
use Laravel\Mcp\Server\Methods\GetPrompt;
|
||||
use Laravel\Mcp\Server\Methods\Initialize;
|
||||
use Laravel\Mcp\Server\Methods\ListPrompts;
|
||||
use Laravel\Mcp\Server\Methods\ListResources;
|
||||
use Laravel\Mcp\Server\Methods\ListResourceTemplates;
|
||||
use Laravel\Mcp\Server\Methods\ListTools;
|
||||
use Laravel\Mcp\Server\Methods\Ping;
|
||||
use Laravel\Mcp\Server\Methods\ReadResource;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\ServerContext;
|
||||
use Laravel\Mcp\Server\Testing\PendingTestResponse;
|
||||
use Laravel\Mcp\Server\Testing\TestResponse;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcNotification;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @mixin PendingTestResponse
|
||||
*/
|
||||
abstract class Server
|
||||
{
|
||||
use ReadsAttributes;
|
||||
|
||||
public const CAPABILITY_TOOLS = 'tools';
|
||||
|
||||
public const CAPABILITY_RESOURCES = 'resources';
|
||||
|
||||
public const CAPABILITY_PROMPTS = 'prompts';
|
||||
|
||||
public const CAPABILITY_COMPLETIONS = 'completions';
|
||||
|
||||
public const CAPABILITY_UI = 'io.modelcontextprotocol/ui';
|
||||
|
||||
protected string $name = 'Laravel MCP Server';
|
||||
|
||||
protected string $version = '0.0.1';
|
||||
|
||||
protected string $instructions = <<<'MARKDOWN'
|
||||
This MCP server lets AI agents interact with our Laravel application.
|
||||
MARKDOWN;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $supportedProtocolVersion = [
|
||||
'2025-11-25',
|
||||
'2025-06-18',
|
||||
'2025-03-26',
|
||||
'2024-11-05',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, bool>|stdClass|string>
|
||||
*/
|
||||
protected array $capabilities = [
|
||||
self::CAPABILITY_TOOLS => [
|
||||
'listChanged' => false,
|
||||
],
|
||||
self::CAPABILITY_RESOURCES => [
|
||||
'listChanged' => false,
|
||||
],
|
||||
self::CAPABILITY_PROMPTS => [
|
||||
'listChanged' => false,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, Tool|class-string<Tool>>
|
||||
*/
|
||||
protected array $tools = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Resource|class-string<Resource>>
|
||||
*/
|
||||
protected array $resources = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Prompt|class-string<Prompt>>
|
||||
*/
|
||||
protected array $prompts = [];
|
||||
|
||||
public int $maxPaginationLength = 50;
|
||||
|
||||
public int $defaultPaginationLength = 15;
|
||||
|
||||
/**
|
||||
* @var array<string, class-string<Method>>
|
||||
*/
|
||||
protected array $methods = [
|
||||
'tools/list' => ListTools::class,
|
||||
'tools/call' => CallTool::class,
|
||||
'resources/list' => ListResources::class,
|
||||
'resources/read' => ReadResource::class,
|
||||
'resources/templates/list' => ListResourceTemplates::class,
|
||||
'prompts/list' => ListPrompts::class,
|
||||
'prompts/get' => GetPrompt::class,
|
||||
'completion/complete' => CompletionComplete::class,
|
||||
'ping' => Ping::class,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Transport $transport,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or modify a server capability.
|
||||
*
|
||||
* Using dot notation like "feature.enabled" will create a nested capability array.
|
||||
* Passing a single key like "anotherFeature" will register an empty object capability.
|
||||
*/
|
||||
public function addCapability(string $key, bool $value = true): void
|
||||
{
|
||||
if (str_contains($key, '.')) {
|
||||
[$root, $child] = explode('.', $key, 2);
|
||||
$existing = $this->capabilities[$root] ?? [];
|
||||
|
||||
if (! is_array($existing)) {
|
||||
$existing = [];
|
||||
}
|
||||
|
||||
$existing[$child] = $value;
|
||||
$this->capabilities[$root] = $existing;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Represent empty capability as an object when JSON encoded
|
||||
$this->capabilities[$key] = (object) [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom JSON-RPC method handler.
|
||||
*
|
||||
* @param class-string<Method> $handler
|
||||
*/
|
||||
public function addMethod(string $method, string $handler): void
|
||||
{
|
||||
$this->methods[$method] = $handler;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$this->boot();
|
||||
$this->detectUiCapability();
|
||||
|
||||
$this->transport->onReceive($this->handle(...));
|
||||
}
|
||||
|
||||
protected function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function handle(string $rawMessage): void
|
||||
{
|
||||
$context = $this->createContext();
|
||||
|
||||
try {
|
||||
$jsonRequest = json_decode($rawMessage, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new JsonRpcException('Parse error: Invalid JSON was received by the server.', -32700);
|
||||
}
|
||||
|
||||
$request = isset($jsonRequest['id'])
|
||||
? JsonRpcRequest::from($jsonRequest, $this->transport->sessionId())
|
||||
: JsonRpcNotification::from($jsonRequest);
|
||||
|
||||
if ($request instanceof JsonRpcNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($request->method === 'initialize') {
|
||||
$this->handleInitializeMessage($request, $context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! isset($this->methods[$request->method])) {
|
||||
throw new JsonRpcException(
|
||||
"The method [{$request->method}] was not found.",
|
||||
-32601,
|
||||
$request->id,
|
||||
);
|
||||
}
|
||||
|
||||
$this->handleMessage($request, $context);
|
||||
} catch (JsonRpcException $e) {
|
||||
$this->transport->send($e->toJsonRpcResponse()->toJson());
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
$config = Container::getInstance()->make('config');
|
||||
|
||||
if ($config->get('app.debug', false)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$jsonRpcResponse = JsonRpcResponse::error(
|
||||
$request->id ?? null,
|
||||
-32603,
|
||||
'Something went wrong while processing the request.',
|
||||
);
|
||||
|
||||
$this->transport->send($jsonRpcResponse->toJson());
|
||||
}
|
||||
}
|
||||
|
||||
public function createContext(): ServerContext
|
||||
{
|
||||
$name = $this->resolveAttribute(Name::class);
|
||||
$version = $this->resolveAttribute(Version::class);
|
||||
$instructions = $this->resolveAttribute(Instructions::class);
|
||||
|
||||
return new ServerContext(
|
||||
supportedProtocolVersions: $this->supportedProtocolVersion,
|
||||
serverCapabilities: $this->capabilities,
|
||||
serverName: $name !== null ? $name->value : $this->name,
|
||||
serverVersion: $version !== null ? $version->value : $this->version,
|
||||
instructions: $instructions !== null ? $instructions->value : $this->instructions,
|
||||
maxPaginationLength: $this->maxPaginationLength,
|
||||
defaultPaginationLength: $this->defaultPaginationLength,
|
||||
tools: $this->tools,
|
||||
resources: $this->resources,
|
||||
prompts: $this->prompts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonRpcException
|
||||
*/
|
||||
protected function handleMessage(JsonRpcRequest $request, ServerContext $context): void
|
||||
{
|
||||
$response = $this->runMethodHandle($request, $context);
|
||||
|
||||
if (! is_iterable($response)) {
|
||||
$this->transport->send($response->toJson());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->transport->stream(function () use ($response): void {
|
||||
foreach ($response as $message) {
|
||||
$this->transport->send($message->toJson());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<JsonRpcResponse>|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);
|
||||
|
||||
$sessionId = $this->generateSessionId();
|
||||
|
||||
Container::getInstance()->make('events')->dispatch(new SessionInitialized(
|
||||
sessionId: $sessionId,
|
||||
clientInfo: $request->params['clientInfo'] ?? null,
|
||||
protocolVersion: $request->params['protocolVersion'] ?? null,
|
||||
clientCapabilities: $request->params['capabilities'] ?? null,
|
||||
));
|
||||
|
||||
$this->transport->send($response->toJson(), $sessionId);
|
||||
}
|
||||
|
||||
protected function generateSessionId(): string
|
||||
{
|
||||
return Str::uuid()->toString();
|
||||
}
|
||||
|
||||
protected function detectUiCapability(): void
|
||||
{
|
||||
if (array_key_exists(self::CAPABILITY_UI, $this->capabilities)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->resources as $resource) {
|
||||
if (is_subclass_of($resource, AppResource::class)) {
|
||||
$this->addCapability(self::CAPABILITY_UI);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $arguments
|
||||
*/
|
||||
public static function __callStatic(string $name, array $arguments): PendingTestResponse|TestResponse
|
||||
{
|
||||
$pendingTestResponse = new PendingTestResponse(
|
||||
Container::getInstance(),
|
||||
static::class,
|
||||
);
|
||||
|
||||
return $pendingTestResponse->$name(...$arguments);
|
||||
}
|
||||
}
|
||||
12
vendor/laravel/mcp/src/Server/Annotations/Annotation.php
vendored
Normal file
12
vendor/laravel/mcp/src/Server/Annotations/Annotation.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Annotations;
|
||||
|
||||
use Laravel\Mcp\Server\Contracts\Annotation as AnnotationContract;
|
||||
|
||||
abstract class Annotation implements AnnotationContract
|
||||
{
|
||||
//
|
||||
}
|
||||
40
vendor/laravel/mcp/src/Server/Annotations/Audience.php
vendored
Normal file
40
vendor/laravel/mcp/src/Server/Annotations/Audience.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Annotations;
|
||||
|
||||
use Attribute;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Mcp\Enums\Role;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Audience extends Annotation
|
||||
{
|
||||
/** @var array<int,string> */
|
||||
public array $value;
|
||||
|
||||
/**
|
||||
* @param Role|array<int, Role> $roles
|
||||
*/
|
||||
public function __construct(Role|array $roles)
|
||||
{
|
||||
$roles = Arr::wrap($roles);
|
||||
|
||||
foreach ($roles as $role) {
|
||||
if (! $role instanceof Role) {
|
||||
throw new InvalidArgumentException(
|
||||
'All values of '.Audience::class.' attributes must be instances of '.Role::class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = array_map(fn (Role $role) => $role->value, $roles);
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'audience';
|
||||
}
|
||||
}
|
||||
28
vendor/laravel/mcp/src/Server/Annotations/LastModified.php
vendored
Normal file
28
vendor/laravel/mcp/src/Server/Annotations/LastModified.php
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Annotations;
|
||||
|
||||
use Attribute;
|
||||
use DateTimeImmutable;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class LastModified extends Annotation
|
||||
{
|
||||
public function __construct(public string $value)
|
||||
{
|
||||
try {
|
||||
new DateTimeImmutable($value);
|
||||
} catch (Exception $exception) {
|
||||
throw new InvalidArgumentException("LastModified must be a valid ISO 8601 timestamp, got '{$value}'", $exception->getCode(), previous: $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'lastModified';
|
||||
}
|
||||
}
|
||||
26
vendor/laravel/mcp/src/Server/Annotations/Priority.php
vendored
Normal file
26
vendor/laravel/mcp/src/Server/Annotations/Priority.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Annotations;
|
||||
|
||||
use Attribute;
|
||||
use InvalidArgumentException;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Priority extends Annotation
|
||||
{
|
||||
public function __construct(public float $value)
|
||||
{
|
||||
if ($value < 0.0 || $value > 1.0) {
|
||||
throw new InvalidArgumentException(
|
||||
"Priority must be between 0.0 and 1.0, got {$value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'priority';
|
||||
}
|
||||
}
|
||||
64
vendor/laravel/mcp/src/Server/AppResource.php
vendored
Normal file
64
vendor/laravel/mcp/src/Server/AppResource.php
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Laravel\Mcp\Server\Attributes\AppMeta as AppMetaAttribute;
|
||||
use Laravel\Mcp\Server\Ui\AppMeta;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Library;
|
||||
|
||||
abstract class AppResource extends Resource
|
||||
{
|
||||
protected string $mimeType = 'text/html;profile=mcp-app';
|
||||
|
||||
protected string $defaultUriScheme = 'ui';
|
||||
|
||||
public function appMeta(): AppMeta
|
||||
{
|
||||
$attribute = $this->resolveAttribute(AppMetaAttribute::class);
|
||||
|
||||
return $attribute?->toAppMeta() ?? new AppMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function resolvedAppMeta(): array
|
||||
{
|
||||
$appMeta = $this->appMeta()->toArray();
|
||||
|
||||
if (! isset($appMeta['domain'])) {
|
||||
$domain = parse_url((string) config('app.url', ''), PHP_URL_HOST) ?: null;
|
||||
|
||||
if ($domain !== null) {
|
||||
$appMeta['domain'] = $domain;
|
||||
}
|
||||
}
|
||||
|
||||
return $appMeta;
|
||||
}
|
||||
|
||||
public function libraryScripts(): string
|
||||
{
|
||||
return implode("\n", array_map(
|
||||
fn (Library $lib): string => implode("\n", $lib->scriptTags()),
|
||||
$this->appMeta()->getLibraries(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = parent::toArray();
|
||||
$appMeta = $this->resolvedAppMeta();
|
||||
|
||||
if ($appMeta !== []) {
|
||||
$data['_meta'] = array_merge($data['_meta'] ?? [], ['ui' => $appMeta]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
82
vendor/laravel/mcp/src/Server/Attributes/AppMeta.php
vendored
Normal file
82
vendor/laravel/mcp/src/Server/Attributes/AppMeta.php
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Laravel\Mcp\Server\Ui\AppMeta as AppMetaData;
|
||||
use Laravel\Mcp\Server\Ui\Csp;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Library;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Permission;
|
||||
use Laravel\Mcp\Server\Ui\Permissions;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class AppMeta
|
||||
{
|
||||
/**
|
||||
* @param array<int, string>|null $connectDomains Domains the app may connect to via fetch, XHR, or WebSocket (CSP connect-src).
|
||||
* @param array<int, string>|null $resourceDomains Domains the app may load images, scripts, styles, and fonts from (CSP default-src).
|
||||
* @param array<int, string>|null $frameDomains Domains the app may embed as nested iframes (CSP frame-src).
|
||||
* @param array<int, string>|null $baseUriDomains Allowed URLs for the document's base element (CSP base-uri).
|
||||
* @param array<int, Permission>|null $permissions
|
||||
* @param array<int, Library> $libraries
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?array $connectDomains = null,
|
||||
public readonly ?array $resourceDomains = null,
|
||||
public readonly ?array $frameDomains = null,
|
||||
public readonly ?array $baseUriDomains = null,
|
||||
public readonly ?array $permissions = null,
|
||||
public readonly ?bool $prefersBorder = null,
|
||||
public readonly ?string $domain = null,
|
||||
public readonly array $libraries = [],
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public function toAppMeta(): AppMetaData
|
||||
{
|
||||
$meta = AppMetaData::make();
|
||||
|
||||
if (($csp = $this->getCsp()) instanceof Csp) {
|
||||
$meta->csp($csp);
|
||||
}
|
||||
|
||||
if ($this->permissions !== null) {
|
||||
$meta->permissions(Permissions::make()->allow(...$this->permissions));
|
||||
}
|
||||
|
||||
if ($this->prefersBorder !== null) {
|
||||
$meta->prefersBorder($this->prefersBorder);
|
||||
}
|
||||
|
||||
if ($this->domain !== null) {
|
||||
$meta->domain($this->domain);
|
||||
}
|
||||
|
||||
if ($this->libraries !== []) {
|
||||
$meta->libraries(...$this->libraries);
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
protected function getCsp(): ?Csp
|
||||
{
|
||||
if (
|
||||
$this->connectDomains === null
|
||||
&& $this->resourceDomains === null
|
||||
&& $this->frameDomains === null
|
||||
&& $this->baseUriDomains === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Csp::make()
|
||||
->connectDomains($this->connectDomains ?? [])
|
||||
->resourceDomains($this->resourceDomains ?? [])
|
||||
->frameDomains($this->frameDomains ?? [])
|
||||
->baseUriDomains($this->baseUriDomains ?? []);
|
||||
}
|
||||
}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Description.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Description.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Description extends ServerAttribute {}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Instructions.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Instructions.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Instructions extends ServerAttribute {}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/MimeType.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/MimeType.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class MimeType extends ServerAttribute {}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Name.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Name.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Name extends ServerAttribute {}
|
||||
23
vendor/laravel/mcp/src/Server/Attributes/RendersApp.php
vendored
Normal file
23
vendor/laravel/mcp/src/Server/Attributes/RendersApp.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Visibility;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class RendersApp
|
||||
{
|
||||
/**
|
||||
* @param class-string $resource
|
||||
* @param array<int, Visibility> $visibility
|
||||
*/
|
||||
public function __construct(
|
||||
public string $resource,
|
||||
public array $visibility = [Visibility::Model, Visibility::App],
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/ServerAttribute.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/ServerAttribute.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
abstract class ServerAttribute
|
||||
{
|
||||
public function __construct(public string $value) {}
|
||||
}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Title.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Title.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Title extends ServerAttribute {}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Uri.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Uri.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Uri extends ServerAttribute {}
|
||||
10
vendor/laravel/mcp/src/Server/Attributes/Version.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Attributes/Version.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Version extends ServerAttribute {}
|
||||
27
vendor/laravel/mcp/src/Server/Completions/ArrayCompletionResponse.php
vendored
Normal file
27
vendor/laravel/mcp/src/Server/Completions/ArrayCompletionResponse.php
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Completions;
|
||||
|
||||
class ArrayCompletionResponse extends CompletionResponse
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $items
|
||||
*/
|
||||
public function __construct(private array $items)
|
||||
{
|
||||
parent::__construct([]);
|
||||
}
|
||||
|
||||
public function resolve(string $value): DirectCompletionResponse
|
||||
{
|
||||
$filtered = CompletionHelper::filterByPrefix($this->items, $value);
|
||||
|
||||
$hasMore = count($filtered) > self::MAX_VALUES;
|
||||
|
||||
$truncated = array_slice($filtered, 0, self::MAX_VALUES);
|
||||
|
||||
return new DirectCompletionResponse($truncated, $hasMore);
|
||||
}
|
||||
}
|
||||
28
vendor/laravel/mcp/src/Server/Completions/CompletionHelper.php
vendored
Normal file
28
vendor/laravel/mcp/src/Server/Completions/CompletionHelper.php
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Completions;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CompletionHelper
|
||||
{
|
||||
/**
|
||||
* @param array<string> $items
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function filterByPrefix(array $items, string $prefix): array
|
||||
{
|
||||
if ($prefix === '') {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$prefixLower = Str::lower($prefix);
|
||||
|
||||
return array_values(array_filter(
|
||||
$items,
|
||||
fn (string $item) => Str::startsWith(Str::lower($item), $prefixLower)
|
||||
));
|
||||
}
|
||||
}
|
||||
90
vendor/laravel/mcp/src/Server/Completions/CompletionResponse.php
vendored
Normal file
90
vendor/laravel/mcp/src/Server/Completions/CompletionResponse.php
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Completions;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
use UnitEnum;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
abstract class CompletionResponse implements Arrayable
|
||||
{
|
||||
protected const MAX_VALUES = 100;
|
||||
|
||||
/**
|
||||
* @param array<int, string> $values
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $values,
|
||||
protected bool $hasMore = false,
|
||||
) {
|
||||
if (count($values) > self::MAX_VALUES) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Completion values cannot exceed %d items (received %d)', self::MAX_VALUES, count($values))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function empty(): CompletionResponse
|
||||
{
|
||||
return new DirectCompletionResponse([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string>|class-string<UnitEnum> $items
|
||||
*/
|
||||
public static function match(array|string $items): CompletionResponse
|
||||
{
|
||||
if (is_string($items)) {
|
||||
return new EnumCompletionResponse($items);
|
||||
}
|
||||
|
||||
return new ArrayCompletionResponse($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string>|string $items
|
||||
*/
|
||||
public static function result(array|string $items): CompletionResponse
|
||||
{
|
||||
if (is_array($items)) {
|
||||
$hasMore = count($items) > self::MAX_VALUES;
|
||||
$truncated = array_slice($items, 0, self::MAX_VALUES);
|
||||
|
||||
return new DirectCompletionResponse($truncated, $hasMore);
|
||||
}
|
||||
|
||||
return new DirectCompletionResponse([$items], false);
|
||||
}
|
||||
|
||||
abstract public function resolve(string $value): CompletionResponse;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function values(): array
|
||||
{
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
public function hasMore(): bool
|
||||
{
|
||||
return $this->hasMore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{values: array<int, string>, total: int, hasMore: bool}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'values' => $this->values,
|
||||
'total' => count($this->values),
|
||||
'hasMore' => $this->hasMore,
|
||||
];
|
||||
}
|
||||
}
|
||||
13
vendor/laravel/mcp/src/Server/Completions/DirectCompletionResponse.php
vendored
Normal file
13
vendor/laravel/mcp/src/Server/Completions/DirectCompletionResponse.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Completions;
|
||||
|
||||
class DirectCompletionResponse extends CompletionResponse
|
||||
{
|
||||
public function resolve(string $value): DirectCompletionResponse
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
40
vendor/laravel/mcp/src/Server/Completions/EnumCompletionResponse.php
vendored
Normal file
40
vendor/laravel/mcp/src/Server/Completions/EnumCompletionResponse.php
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Completions;
|
||||
|
||||
use BackedEnum;
|
||||
use InvalidArgumentException;
|
||||
use UnitEnum;
|
||||
|
||||
class EnumCompletionResponse extends CompletionResponse
|
||||
{
|
||||
/**
|
||||
* @param class-string<UnitEnum> $enumClass
|
||||
*/
|
||||
public function __construct(private string $enumClass)
|
||||
{
|
||||
if (! enum_exists($enumClass)) {
|
||||
throw new InvalidArgumentException("Class [{$enumClass}] is not an enum.");
|
||||
}
|
||||
|
||||
parent::__construct([]);
|
||||
}
|
||||
|
||||
public function resolve(string $value): DirectCompletionResponse
|
||||
{
|
||||
$enumValues = array_map(
|
||||
fn (UnitEnum $case): string => $case instanceof BackedEnum ? (string) $case->value : $case->name,
|
||||
$this->enumClass::cases()
|
||||
);
|
||||
|
||||
$filtered = CompletionHelper::filterByPrefix($enumValues, $value);
|
||||
|
||||
$hasMore = count($filtered) > self::MAX_VALUES;
|
||||
|
||||
$truncated = array_slice($filtered, 0, self::MAX_VALUES);
|
||||
|
||||
return new DirectCompletionResponse($truncated, $hasMore);
|
||||
}
|
||||
}
|
||||
70
vendor/laravel/mcp/src/Server/Concerns/HasAnnotations.php
vendored
Normal file
70
vendor/laravel/mcp/src/Server/Concerns/HasAnnotations.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Concerns;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Mcp\Server\Contracts\Annotation as AnnotationContract;
|
||||
use ReflectionAttribute;
|
||||
use ReflectionClass;
|
||||
|
||||
trait HasAnnotations
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function annotations(): array
|
||||
{
|
||||
$reflection = new ReflectionClass($this);
|
||||
|
||||
/** @var Collection<int, AnnotationContract> $annotations */
|
||||
$annotations = collect($reflection->getAttributes())
|
||||
->map(fn (ReflectionAttribute $attributeReflection): object => $attributeReflection->newInstance())
|
||||
->filter(fn (object $attribute): bool => $attribute instanceof AnnotationContract)
|
||||
->values();
|
||||
|
||||
// @phpstan-ignore argument.templateType
|
||||
return $annotations
|
||||
->each(function (AnnotationContract $attribute): void {
|
||||
$this->validateAnnotationUsage($attribute);
|
||||
})
|
||||
->mapWithKeys(fn (AnnotationContract $attribute): array => [ // @phpstan-ignore argument.templateType
|
||||
$attribute->key() => $attribute->value, // @phpstan-ignore property.notFound
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function validateAnnotationUsage(AnnotationContract $attribute): void
|
||||
{
|
||||
$allowedAnnotations = $this->allowedAnnotations();
|
||||
|
||||
foreach ($allowedAnnotations as $allowedAnnotationClass) {
|
||||
if ($attribute instanceof $allowedAnnotationClass) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$allowedClasses = empty($allowedAnnotations)
|
||||
? 'none'
|
||||
: implode(', ', $allowedAnnotations);
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Annotation [%s] cannot be used on [%s]. Allowed annotation types: [%s]',
|
||||
$attribute::class,
|
||||
$this::class,
|
||||
$allowedClasses
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string>
|
||||
*/
|
||||
protected function allowedAnnotations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
48
vendor/laravel/mcp/src/Server/Concerns/HasMeta.php
vendored
Normal file
48
vendor/laravel/mcp/src/Server/Concerns/HasMeta.php
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Concerns;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
trait HasMeta
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected ?array $meta = null;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string $meta
|
||||
*/
|
||||
public function setMeta(array|string $meta, mixed $value = null): void
|
||||
{
|
||||
$this->meta ??= [];
|
||||
|
||||
if (! is_array($meta)) {
|
||||
if (is_null($value)) {
|
||||
throw new InvalidArgumentException('Value is required when using key-value signature.');
|
||||
}
|
||||
|
||||
$this->meta[$meta] = $value;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->meta = array_merge($this->meta, $meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array<string, mixed>
|
||||
*
|
||||
* @param T $baseArray
|
||||
* @return T&array{_meta?: array<string, mixed>}
|
||||
*/
|
||||
public function mergeMeta(array $baseArray): array
|
||||
{
|
||||
return ($meta = $this->meta)
|
||||
? [...$baseArray, '_meta' => $meta]
|
||||
: $baseArray;
|
||||
}
|
||||
}
|
||||
36
vendor/laravel/mcp/src/Server/Concerns/HasStructuredContent.php
vendored
Normal file
36
vendor/laravel/mcp/src/Server/Concerns/HasStructuredContent.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Concerns;
|
||||
|
||||
trait HasStructuredContent
|
||||
{
|
||||
/**
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected ?array $structuredContent = null;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $structuredContent
|
||||
*/
|
||||
public function setStructuredContent(array $structuredContent): void
|
||||
{
|
||||
$this->structuredContent ??= [];
|
||||
|
||||
$this->structuredContent = array_merge($this->structuredContent, $structuredContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $baseArray
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function mergeStructuredContent(array $baseArray): array
|
||||
{
|
||||
if ($this->structuredContent === null) {
|
||||
return $baseArray;
|
||||
}
|
||||
|
||||
return array_merge($baseArray, ['structuredContent' => $this->structuredContent]);
|
||||
}
|
||||
}
|
||||
42
vendor/laravel/mcp/src/Server/Concerns/ReadsAttributes.php
vendored
Normal file
42
vendor/laravel/mcp/src/Server/Concerns/ReadsAttributes.php
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Concerns;
|
||||
|
||||
use ReflectionClass;
|
||||
|
||||
trait ReadsAttributes
|
||||
{
|
||||
/**
|
||||
* @var array<string, object|null>
|
||||
*/
|
||||
protected static array $attributeCache = [];
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T> $attributeClass
|
||||
* @return T|null
|
||||
*/
|
||||
protected function resolveAttribute(string $attributeClass): mixed
|
||||
{
|
||||
$cacheKey = static::class.'@'.$attributeClass;
|
||||
|
||||
if (array_key_exists($cacheKey, static::$attributeCache)) {
|
||||
return static::$attributeCache[$cacheKey]; // @phpstan-ignore return.type
|
||||
}
|
||||
|
||||
$reflection = new ReflectionClass($this);
|
||||
|
||||
do {
|
||||
$attributes = $reflection->getAttributes($attributeClass);
|
||||
|
||||
if ($attributes !== []) {
|
||||
return static::$attributeCache[$cacheKey] = $attributes[0]->newInstance();
|
||||
}
|
||||
} while ($reflection = $reflection->getParentClass());
|
||||
|
||||
return static::$attributeCache[$cacheKey] = null;
|
||||
}
|
||||
}
|
||||
66
vendor/laravel/mcp/src/Server/Content/Audio.php
vendored
Normal file
66
vendor/laravel/mcp/src/Server/Content/Audio.php
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Content;
|
||||
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class Audio implements Content
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
public function __construct(protected string $data, protected string $mimeType = 'audio/wav')
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'blob' => base64_encode($this->data),
|
||||
'uri' => $resource->uri(),
|
||||
'mimeType' => $this->mimeType,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'type' => 'audio',
|
||||
'data' => base64_encode($this->data),
|
||||
'mimeType' => $this->mimeType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
vendor/laravel/mcp/src/Server/Content/Blob.php
vendored
Normal file
70
vendor/laravel/mcp/src/Server/Content/Blob.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Content;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class Blob implements Content
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
public function __construct(protected string $content)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array
|
||||
{
|
||||
throw new InvalidArgumentException(
|
||||
'Blob content may not be used in tools.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array
|
||||
{
|
||||
throw new InvalidArgumentException(
|
||||
'Blob content may not be used in prompts.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'blob' => base64_encode($this->content),
|
||||
'uri' => $resource->uri(),
|
||||
'mimeType' => $resource->mimeType(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'type' => 'blob',
|
||||
'blob' => $this->content,
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
vendor/laravel/mcp/src/Server/Content/Image.php
vendored
Normal file
66
vendor/laravel/mcp/src/Server/Content/Image.php
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Content;
|
||||
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class Image implements Content
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
public function __construct(protected string $data, protected string $mimeType = 'image/png')
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'blob' => base64_encode($this->data),
|
||||
'uri' => $resource->uri(),
|
||||
'mimeType' => $this->mimeType,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'type' => 'image',
|
||||
'data' => base64_encode($this->data),
|
||||
'mimeType' => $this->mimeType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
vendor/laravel/mcp/src/Server/Content/Notification.php
vendored
Normal file
70
vendor/laravel/mcp/src/Server/Content/Notification.php
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Content;
|
||||
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class Notification implements Content
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function __construct(protected string $method, protected array $params)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$params = $this->params;
|
||||
|
||||
if ($this->meta !== null && $this->meta !== [] && ! isset($params['_meta'])) {
|
||||
$params['_meta'] = $this->meta;
|
||||
}
|
||||
|
||||
return [
|
||||
'method' => $this->method,
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
}
|
||||
65
vendor/laravel/mcp/src/Server/Content/Text.php
vendored
Normal file
65
vendor/laravel/mcp/src/Server/Content/Text.php
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Content;
|
||||
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Contracts\Content;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class Text implements Content
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
public function __construct(protected string $text)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'text' => $this->text,
|
||||
'uri' => $resource->uri(),
|
||||
'mimeType' => $resource->mimeType(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->mergeMeta([
|
||||
'type' => 'text',
|
||||
'text' => $this->text,
|
||||
]);
|
||||
}
|
||||
}
|
||||
10
vendor/laravel/mcp/src/Server/Contracts/Annotation.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Contracts/Annotation.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
interface Annotation
|
||||
{
|
||||
public function key(): string;
|
||||
}
|
||||
15
vendor/laravel/mcp/src/Server/Contracts/Completable.php
vendored
Normal file
15
vendor/laravel/mcp/src/Server/Contracts/Completable.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
use Laravel\Mcp\Server\Completions\CompletionResponse;
|
||||
|
||||
interface Completable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
public function complete(string $argument, string $value, array $context): CompletionResponse;
|
||||
}
|
||||
39
vendor/laravel/mcp/src/Server/Contracts/Content.php
vendored
Normal file
39
vendor/laravel/mcp/src/Server/Contracts/Content.php
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* @extends Arrayable<string, mixed>
|
||||
*/
|
||||
interface Content extends Arrayable, Stringable
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toTool(Tool $tool): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toPrompt(Prompt $prompt): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toResource(Resource $resource): array;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string $meta
|
||||
*/
|
||||
public function setMeta(array|string $meta, mixed $value = null): void;
|
||||
|
||||
public function __toString(): string;
|
||||
}
|
||||
10
vendor/laravel/mcp/src/Server/Contracts/Errable.php
vendored
Normal file
10
vendor/laravel/mcp/src/Server/Contracts/Errable.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
interface Errable
|
||||
{
|
||||
//
|
||||
}
|
||||
15
vendor/laravel/mcp/src/Server/Contracts/HasUriTemplate.php
vendored
Normal file
15
vendor/laravel/mcp/src/Server/Contracts/HasUriTemplate.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
use Laravel\Mcp\Support\UriTemplate;
|
||||
|
||||
interface HasUriTemplate
|
||||
{
|
||||
/**
|
||||
* Get the URI pattern for the resource template.
|
||||
*/
|
||||
public function uriTemplate(): UriTemplate;
|
||||
}
|
||||
20
vendor/laravel/mcp/src/Server/Contracts/Method.php
vendored
Normal file
20
vendor/laravel/mcp/src/Server/Contracts/Method.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
|
||||
use Laravel\Mcp\Server\ServerContext;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
|
||||
|
||||
interface Method
|
||||
{
|
||||
/**
|
||||
* @return iterable<JsonRpcResponse>|JsonRpcResponse
|
||||
*
|
||||
* @throws JsonRpcException
|
||||
*/
|
||||
public function handle(JsonRpcRequest $request, ServerContext $context): iterable|JsonRpcResponse;
|
||||
}
|
||||
20
vendor/laravel/mcp/src/Server/Contracts/Transport.php
vendored
Normal file
20
vendor/laravel/mcp/src/Server/Contracts/Transport.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Contracts;
|
||||
|
||||
use Closure;
|
||||
|
||||
interface Transport
|
||||
{
|
||||
public function onReceive(Closure $handler): void;
|
||||
|
||||
public function run(); // @phpstan-ignore-line
|
||||
|
||||
public function send(string $message, ?string $sessionId = null): void;
|
||||
|
||||
public function sessionId(): ?string;
|
||||
|
||||
public function stream(Closure $stream): void;
|
||||
}
|
||||
33
vendor/laravel/mcp/src/Server/Exceptions/JsonRpcException.php
vendored
Normal file
33
vendor/laravel/mcp/src/Server/Exceptions/JsonRpcException.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
|
||||
|
||||
class JsonRpcException extends Exception
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code,
|
||||
protected mixed $requestId = null,
|
||||
protected ?array $data = null
|
||||
) {
|
||||
parent::__construct($message, $code);
|
||||
}
|
||||
|
||||
public function toJsonRpcResponse(): JsonRpcResponse
|
||||
{
|
||||
return JsonRpcResponse::error(
|
||||
id: $this->requestId,
|
||||
code: $this->getCode(),
|
||||
message: $this->getMessage(),
|
||||
data: $this->data,
|
||||
);
|
||||
}
|
||||
}
|
||||
156
vendor/laravel/mcp/src/Server/Http/Controllers/OAuthRegisterController.php
vendored
Normal file
156
vendor/laravel/mcp/src/Server/Http/Controllers/OAuthRegisterController.php
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Http\Controllers;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class OAuthRegisterController
|
||||
{
|
||||
/**
|
||||
* Register a new OAuth client for a third-party application.
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'client_name' => ['nullable', 'string', 'min:1', 'max:255', 'required_without:name'],
|
||||
'name' => ['nullable', 'string', 'min:1', 'max:255', 'required_without:client_name'],
|
||||
'redirect_uris' => ['required', 'array', 'min:1'],
|
||||
'redirect_uris.*' => ['required', 'string', function (string $attribute, $value, $fail): void {
|
||||
if (! $this->isValidRedirectUri($value)) {
|
||||
$fail($attribute.' is not a valid URL.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! in_array(parse_url($value, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array('*', config('mcp.redirect_domains', []), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasLocalhostDomain() && $this->isLocalhostUrl($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Str::startsWith($value, $this->allowedDomains())) {
|
||||
$fail($attribute.' is not a permitted redirect domain.');
|
||||
}
|
||||
}],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors = $validator->errors();
|
||||
|
||||
$isRedirectError = collect($errors->keys())->contains(
|
||||
fn (string $key): bool => str_starts_with($key, 'redirect_uris')
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => $isRedirectError ? 'invalid_redirect_uri' : 'invalid_client_metadata',
|
||||
'error_description' => $errors->first(),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$validated = $validator->validated();
|
||||
|
||||
if (class_exists('Laravel\Passport\ClientRepository') === false) {
|
||||
return response()->json([
|
||||
'error' => 'server_error',
|
||||
'error_description' => 'OAuth support (Passport) is not installed.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
$clients = Container::getInstance()->make(
|
||||
'Laravel\Passport\ClientRepository'
|
||||
);
|
||||
|
||||
$client = $clients->createAuthorizationCodeGrantClient(
|
||||
name: $validated['client_name'] ?? $validated['name'],
|
||||
redirectUris: $validated['redirect_uris'],
|
||||
confidential: false,
|
||||
user: null,
|
||||
enableDeviceFlow: false,
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'client_id' => (string) $client->id,
|
||||
'grant_types' => $client->grant_types,
|
||||
'response_types' => ['code'],
|
||||
'redirect_uris' => $client->redirect_uris,
|
||||
'scope' => 'mcp:use',
|
||||
'token_endpoint_auth_method' => 'none',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function isValidRedirectUri(string $value): bool
|
||||
{
|
||||
$scheme = parse_url($value, PHP_URL_SCHEME);
|
||||
|
||||
if (! is_string($scheme)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($scheme, ['http', 'https'], true)) {
|
||||
return Str::isUrl($value, ['http', 'https']);
|
||||
}
|
||||
|
||||
/** @var array<int, string> */
|
||||
$allowedSchemes = config('mcp.custom_schemes', []);
|
||||
$host = parse_url($value, PHP_URL_HOST);
|
||||
|
||||
return in_array($scheme, $allowedSchemes, true) && is_string($host) && $host !== '';
|
||||
}
|
||||
|
||||
protected function isLocalhostUrl(string $url): bool
|
||||
{
|
||||
return Str::startsWith($url, [
|
||||
'http://localhost:',
|
||||
'http://localhost/',
|
||||
'http://127.0.0.1:',
|
||||
'http://127.0.0.1/',
|
||||
'http://[::1]:',
|
||||
'http://[::1]/',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed redirect domains.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function allowedDomains(): array
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
$allowedDomains = config('mcp.redirect_domains', []);
|
||||
|
||||
return collect($allowedDomains)
|
||||
->map(fn (string $domain): string => Str::endsWith($domain, '/')
|
||||
? $domain
|
||||
: "{$domain}/"
|
||||
)
|
||||
->all();
|
||||
}
|
||||
|
||||
private function hasLocalhostDomain(): bool
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
$domains = config('mcp.redirect_domains', []);
|
||||
|
||||
return collect($domains)->contains(fn (string $domain): bool => in_array(
|
||||
rtrim(Str::after($domain, '://'), '/'),
|
||||
['localhost', '127.0.0.1', '[::1]'],
|
||||
true,
|
||||
));
|
||||
}
|
||||
}
|
||||
120
vendor/laravel/mcp/src/Server/McpServiceProvider.php
vendored
Normal file
120
vendor/laravel/mcp/src/Server/McpServiceProvider.php
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Mcp\Console\Commands\InspectorCommand;
|
||||
use Laravel\Mcp\Console\Commands\MakeAppResourceCommand;
|
||||
use Laravel\Mcp\Console\Commands\MakePromptCommand;
|
||||
use Laravel\Mcp\Console\Commands\MakeResourceCommand;
|
||||
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
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(Registrar::class, fn (): Registrar => new Registrar);
|
||||
|
||||
$this->app->singleton('mcp.sdk', fn (): string => (string) file_get_contents(__DIR__.'/../../resources/js/mcp-sdk.min.js'));
|
||||
|
||||
$this->mergeConfigFrom(__DIR__.'/../../config/mcp.php', 'mcp');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerMcpScope();
|
||||
$this->registerRoutes();
|
||||
$this->registerContainerCallbacks();
|
||||
$this->registerViews();
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->registerCommands();
|
||||
$this->registerPublishing();
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerPublishing(): void
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__.'/../../routes/ai.php' => base_path('routes/ai.php'),
|
||||
], 'ai-routes');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../../resources/views/mcp/authorize.blade.php' => resource_path('views/mcp/authorize.blade.php'),
|
||||
__DIR__.'/../../resources/views/mcp/components/app.blade.php' => resource_path('views/vendor/mcp/components/app.blade.php'),
|
||||
], 'mcp-views');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../../stubs/mcp-prompt.stub' => base_path('stubs/mcp-prompt.stub'),
|
||||
__DIR__.'/../../stubs/mcp-resource.stub' => base_path('stubs/mcp-resource.stub'),
|
||||
__DIR__.'/../../stubs/mcp-server.stub' => base_path('stubs/mcp-server.stub'),
|
||||
__DIR__.'/../../stubs/mcp-tool.stub' => base_path('stubs/mcp-tool.stub'),
|
||||
__DIR__.'/../../stubs/mcp-app-resource.stub' => base_path('stubs/mcp-app-resource.stub'),
|
||||
__DIR__.'/../../stubs/mcp-app-resource.view.stub' => base_path('stubs/mcp-app-resource.view.stub'),
|
||||
], 'mcp-stubs');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../../config/mcp.php' => config_path('mcp.php'),
|
||||
], 'mcp-config');
|
||||
}
|
||||
|
||||
protected function registerRoutes(): void
|
||||
{
|
||||
$path = base_path('routes/ai.php');
|
||||
|
||||
if (! file_exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->app->runningInConsole() && $this->app->routesAreCached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
$request->setMeta($currentRequest->meta());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
$this->commands([
|
||||
StartCommand::class,
|
||||
MakeServerCommand::class,
|
||||
MakeToolCommand::class,
|
||||
MakePromptCommand::class,
|
||||
MakeResourceCommand::class,
|
||||
MakeAppResourceCommand::class,
|
||||
InspectorCommand::class,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function registerViews(): void
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../../resources/views/mcp', 'mcp');
|
||||
}
|
||||
|
||||
protected function registerMcpScope(): void
|
||||
{
|
||||
$this->app->booted(function (): void {
|
||||
Registrar::ensureMcpScope();
|
||||
});
|
||||
}
|
||||
}
|
||||
79
vendor/laravel/mcp/src/Server/Methods/CallTool.php
vendored
Normal file
79
vendor/laravel/mcp/src/Server/Methods/CallTool.php
vendored
Normal 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()),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
115
vendor/laravel/mcp/src/Server/Methods/CompletionComplete.php
vendored
Normal file
115
vendor/laravel/mcp/src/Server/Methods/CompletionComplete.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
127
vendor/laravel/mcp/src/Server/Methods/Concerns/InteractsWithResponses.php
vendored
Normal file
127
vendor/laravel/mcp/src/Server/Methods/Concerns/InteractsWithResponses.php
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
24
vendor/laravel/mcp/src/Server/Methods/Concerns/ResolvesPrompts.php
vendored
Normal file
24
vendor/laravel/mcp/src/Server/Methods/Concerns/ResolvesPrompts.php
vendored
Normal 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.")
|
||||
);
|
||||
}
|
||||
}
|
||||
29
vendor/laravel/mcp/src/Server/Methods/Concerns/ResolvesResources.php
vendored
Normal file
29
vendor/laravel/mcp/src/Server/Methods/Concerns/ResolvesResources.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
64
vendor/laravel/mcp/src/Server/Methods/GetPrompt.php
vendored
Normal file
64
vendor/laravel/mcp/src/Server/Methods/GetPrompt.php
vendored
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
48
vendor/laravel/mcp/src/Server/Methods/Initialize.php
vendored
Normal file
48
vendor/laravel/mcp/src/Server/Methods/Initialize.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
25
vendor/laravel/mcp/src/Server/Methods/ListPrompts.php
vendored
Normal file
25
vendor/laravel/mcp/src/Server/Methods/ListPrompts.php
vendored
Normal 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'));
|
||||
}
|
||||
}
|
||||
25
vendor/laravel/mcp/src/Server/Methods/ListResourceTemplates.php
vendored
Normal file
25
vendor/laravel/mcp/src/Server/Methods/ListResourceTemplates.php
vendored
Normal 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'));
|
||||
}
|
||||
}
|
||||
25
vendor/laravel/mcp/src/Server/Methods/ListResources.php
vendored
Normal file
25
vendor/laravel/mcp/src/Server/Methods/ListResources.php
vendored
Normal 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'));
|
||||
}
|
||||
}
|
||||
25
vendor/laravel/mcp/src/Server/Methods/ListTools.php
vendored
Normal file
25
vendor/laravel/mcp/src/Server/Methods/ListTools.php
vendored
Normal 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'));
|
||||
}
|
||||
}
|
||||
18
vendor/laravel/mcp/src/Server/Methods/Ping.php
vendored
Normal file
18
vendor/laravel/mcp/src/Server/Methods/Ping.php
vendored
Normal 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, []);
|
||||
}
|
||||
}
|
||||
110
vendor/laravel/mcp/src/Server/Methods/ReadResource.php
vendored
Normal file
110
vendor/laravel/mcp/src/Server/Methods/ReadResource.php
vendored
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
vendor/laravel/mcp/src/Server/Middleware/AddWwwAuthenticateHeader.php
vendored
Normal file
43
vendor/laravel/mcp/src/Server/Middleware/AddWwwAuthenticateHeader.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AddWwwAuthenticateHeader
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (\Illuminate\Http\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
if ($response->getStatusCode() !== 401) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$isOauth = app('router')->has('mcp.oauth.protected-resource.nested');
|
||||
if ($isOauth) {
|
||||
$response->header(
|
||||
'WWW-Authenticate',
|
||||
'Bearer realm="mcp", resource_metadata="'.route('mcp.oauth.protected-resource.nested', ['path' => $request->path()]).'"'
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Sanctum, can't share discover URL
|
||||
$response->header(
|
||||
'WWW-Authenticate',
|
||||
'Bearer realm="mcp", error="invalid_token"'
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
34
vendor/laravel/mcp/src/Server/Middleware/ReorderJsonAccept.php
vendored
Normal file
34
vendor/laravel/mcp/src/Server/Middleware/ReorderJsonAccept.php
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ReorderJsonAccept
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$accept = $request->header('Accept');
|
||||
if (is_string($accept) && str_contains($accept, ',')) {
|
||||
$accept = array_map(trim(...), explode(',', $accept));
|
||||
}
|
||||
|
||||
if (! is_array($accept)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
usort($accept, fn ($a, $b): int => str_contains((string) $b, 'application/json') <=> str_contains((string) $a, 'application/json'));
|
||||
$request->headers->set('Accept', implode(', ', $accept));
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
73
vendor/laravel/mcp/src/Server/Pagination/CursorPaginator.php
vendored
Normal file
73
vendor/laravel/mcp/src/Server/Pagination/CursorPaginator.php
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Pagination;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class CursorPaginator
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, mixed> $items
|
||||
*/
|
||||
public function __construct(protected Collection $items, protected int $perPage = 10, protected ?string $cursor = null)
|
||||
{
|
||||
$this->items = $items->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function paginate(string $key = 'items'): array
|
||||
{
|
||||
$startOffset = $this->getStartOffsetFromCursor();
|
||||
|
||||
$paginatedItems = $this->items->slice($startOffset, $this->perPage);
|
||||
|
||||
$hasMorePages = $this->items->count() > ($startOffset + $this->perPage);
|
||||
|
||||
$result = [$key => $paginatedItems->values()->toArray()];
|
||||
|
||||
if ($hasMorePages) {
|
||||
$result['nextCursor'] = $this->createCursor($startOffset + $this->perPage);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getStartOffsetFromCursor(): int
|
||||
{
|
||||
if (! is_string($this->cursor)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$decodedCursor = base64_decode($this->cursor, true);
|
||||
|
||||
if ($decodedCursor === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cursorData = json_decode($decodedCursor, true);
|
||||
|
||||
if (! is_array($cursorData)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) ($cursorData['offset'] ?? 0);
|
||||
} catch (Throwable) {
|
||||
//
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function createCursor(int $offset): string
|
||||
{
|
||||
$cursorData = ['offset' => $offset];
|
||||
|
||||
return base64_encode((string) json_encode($cursorData));
|
||||
}
|
||||
}
|
||||
83
vendor/laravel/mcp/src/Server/Primitive.php
vendored
Normal file
83
vendor/laravel/mcp/src/Server/Primitive.php
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Title;
|
||||
use Laravel\Mcp\Server\Concerns\HasMeta;
|
||||
use Laravel\Mcp\Server\Concerns\ReadsAttributes;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
abstract class Primitive implements Arrayable
|
||||
{
|
||||
use HasMeta;
|
||||
use ReadsAttributes;
|
||||
|
||||
protected string $name = '';
|
||||
|
||||
protected string $title = '';
|
||||
|
||||
protected string $description = '';
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
$attribute = $this->resolveAttribute(Name::class);
|
||||
|
||||
return $attribute !== null
|
||||
? $attribute->value
|
||||
: ($this->name !== '' ? $this->name : Str::kebab(class_basename($this)));
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
$attribute = $this->resolveAttribute(Title::class);
|
||||
|
||||
return $attribute !== null
|
||||
? $attribute->value
|
||||
: ($this->title !== '' ? $this->title : Str::headline(class_basename($this)));
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
$attribute = $this->resolveAttribute(Description::class);
|
||||
|
||||
return $attribute !== null
|
||||
? $attribute->value
|
||||
: ($this->description !== '' ? $this->description : Str::headline(class_basename($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function meta(): ?array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function eligibleForRegistration(): bool
|
||||
{
|
||||
if (method_exists($this, 'shouldRegister')) {
|
||||
return Container::getInstance()->call([$this, 'shouldRegister']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract public function toMethodCall(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract public function toArray(): array;
|
||||
}
|
||||
46
vendor/laravel/mcp/src/Server/Prompt.php
vendored
Normal file
46
vendor/laravel/mcp/src/Server/Prompt.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
use Laravel\Mcp\Server\Prompts\Arguments;
|
||||
|
||||
abstract class Prompt extends Primitive
|
||||
{
|
||||
/**
|
||||
* @return array<int, Argument>
|
||||
*/
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toMethodCall(): array
|
||||
{
|
||||
return ['name' => $this->name()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{name: string, title: string, description: string, arguments: array<int, array{name: string, description: string, required: bool, _meta?: array<string, mixed>}>}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
// @phpstan-ignore return.type
|
||||
return $this->mergeMeta([
|
||||
'name' => $this->name(),
|
||||
'title' => $this->title(),
|
||||
'description' => $this->description(),
|
||||
'arguments' => array_map(
|
||||
fn (Argument $argument): array => $argument->toArray(),
|
||||
$this->arguments(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
vendor/laravel/mcp/src/Server/Prompts/Argument.php
vendored
Normal file
33
vendor/laravel/mcp/src/Server/Prompts/Argument.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Prompts;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class Argument implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $description,
|
||||
public bool $required = false,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{name: string, description: string, required: bool}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'required' => $this->required,
|
||||
];
|
||||
}
|
||||
}
|
||||
191
vendor/laravel/mcp/src/Server/Registrar.php
vendored
Normal file
191
vendor/laravel/mcp/src/Server/Registrar.php
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as Router;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Contracts\Transport;
|
||||
use Laravel\Mcp\Server\Http\Controllers\OAuthRegisterController;
|
||||
use Laravel\Mcp\Server\Middleware\AddWwwAuthenticateHeader;
|
||||
use Laravel\Mcp\Server\Middleware\ReorderJsonAccept;
|
||||
use Laravel\Mcp\Server\Transport\HttpTransport;
|
||||
use Laravel\Mcp\Server\Transport\StdioTransport;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
class Registrar
|
||||
{
|
||||
/** @var array<string, callable> */
|
||||
protected array $localServers = [];
|
||||
|
||||
/** @var array<string, Route> */
|
||||
protected array $httpServers = [];
|
||||
|
||||
/**
|
||||
* @param class-string<Server> $serverClass
|
||||
*/
|
||||
public function web(string $route, string $serverClass): Route
|
||||
{
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server
|
||||
Router::get($route, fn (): Response => response('', 405)->header('Allow', 'POST'));
|
||||
|
||||
Router::delete($route, fn (): Response => response('', 405)->header('Allow', 'POST'));
|
||||
|
||||
$route = Router::post($route, static fn (): mixed => static::startServer(
|
||||
$serverClass,
|
||||
static fn (): HttpTransport => new HttpTransport(
|
||||
$request = request(),
|
||||
// @phpstan-ignore-next-line
|
||||
(string) $request->header('MCP-Session-Id')
|
||||
),
|
||||
))->middleware([
|
||||
ReorderJsonAccept::class,
|
||||
AddWwwAuthenticateHeader::class,
|
||||
]);
|
||||
|
||||
assert($route instanceof Route);
|
||||
|
||||
$this->httpServers[$route->uri()] = $route;
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Server> $serverClass
|
||||
*/
|
||||
public function local(string $handle, string $serverClass): void
|
||||
{
|
||||
$this->localServers[$handle] = fn (): mixed => static::startServer($serverClass, fn (): StdioTransport => new StdioTransport(
|
||||
Str::uuid()->toString(),
|
||||
));
|
||||
}
|
||||
|
||||
public function getLocalServer(string $handle): ?callable
|
||||
{
|
||||
return $this->localServers[$handle] ?? null;
|
||||
}
|
||||
|
||||
public function getWebServer(string $route): ?Route
|
||||
{
|
||||
return $this->httpServers[$route] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, callable|Route>
|
||||
*/
|
||||
public function servers(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->localServers,
|
||||
$this->httpServers,
|
||||
);
|
||||
}
|
||||
|
||||
public function oauthRoutes(string $oauthPrefix = 'oauth'): void
|
||||
{
|
||||
static::ensureMcpScope();
|
||||
$hasExactProtectedResourceRoute = $this->hasGetRoute('.well-known/oauth-protected-resource');
|
||||
$hasExactAuthorizationServerRoute = $this->hasGetRoute('.well-known/oauth-authorization-server');
|
||||
|
||||
if (! $hasExactProtectedResourceRoute) {
|
||||
Router::get('/.well-known/oauth-protected-resource', static fn () => response()->json(static::protectedResourceMetadata('')))
|
||||
->name('mcp.oauth.protected-resource');
|
||||
}
|
||||
|
||||
if (! $hasExactAuthorizationServerRoute) {
|
||||
Router::get('/.well-known/oauth-authorization-server', static fn () => response()->json(static::authorizationServerMetadata($oauthPrefix)))
|
||||
->name('mcp.oauth.authorization-server');
|
||||
}
|
||||
|
||||
Router::get('/.well-known/oauth-protected-resource/{path}', static fn (string $path) => response()->json(static::protectedResourceMetadata($path)))
|
||||
->where('path', '.*')
|
||||
->name('mcp.oauth.protected-resource.nested');
|
||||
|
||||
Router::get('/.well-known/oauth-authorization-server/{path}', static fn (string $path) => response()->json(static::authorizationServerMetadata($oauthPrefix)))
|
||||
->where('path', '.*')
|
||||
->name('mcp.oauth.authorization-server.nested');
|
||||
|
||||
Router::post($oauthPrefix.'/register', OAuthRegisterController::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>|string>
|
||||
*/
|
||||
protected static function authorizationServerMetadata(string $oauthPrefix): array
|
||||
{
|
||||
return [
|
||||
'issuer' => config('mcp.authorization_server') ?? url('/'),
|
||||
'authorization_endpoint' => route('passport.authorizations.authorize'),
|
||||
'token_endpoint' => route('passport.token'),
|
||||
'registration_endpoint' => url($oauthPrefix.'/register'),
|
||||
'response_types_supported' => ['code'],
|
||||
'code_challenge_methods_supported' => ['S256'],
|
||||
'scopes_supported' => ['mcp:use'],
|
||||
'grant_types_supported' => ['authorization_code', 'refresh_token'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>|string>
|
||||
*/
|
||||
protected static function protectedResourceMetadata(string $path): array
|
||||
{
|
||||
return [
|
||||
'resource' => url('/'.$path),
|
||||
'authorization_servers' => [config('mcp.authorization_server') ?? url('/')],
|
||||
'scopes_supported' => ['mcp:use'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function hasGetRoute(string $uri): bool
|
||||
{
|
||||
foreach (Router::getRoutes()->getRoutes() as $route) {
|
||||
if ($route->uri() === $uri && in_array('GET', $route->methods(), true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function ensureMcpScope(): array
|
||||
{
|
||||
if (class_exists(Passport::class) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$current = Passport::$scopes ?? [];
|
||||
|
||||
if (! array_key_exists('mcp:use', $current)) {
|
||||
$current['mcp:use'] = 'Use MCP server';
|
||||
Passport::tokensCan($current);
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Server> $serverClass
|
||||
* @param callable(): Transport $transportFactory
|
||||
*/
|
||||
protected static function startServer(string $serverClass, callable $transportFactory): mixed
|
||||
{
|
||||
$transport = $transportFactory();
|
||||
|
||||
$server = Container::getInstance()->make($serverClass, [
|
||||
'transport' => $transport,
|
||||
]);
|
||||
|
||||
$server->start();
|
||||
|
||||
return $transport->run();
|
||||
}
|
||||
}
|
||||
99
vendor/laravel/mcp/src/Server/Resource.php
vendored
Normal file
99
vendor/laravel/mcp/src/Server/Resource.php
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Mcp\Server\Annotations\Annotation;
|
||||
use Laravel\Mcp\Server\Attributes\MimeType;
|
||||
use Laravel\Mcp\Server\Attributes\Uri;
|
||||
use Laravel\Mcp\Server\Concerns\HasAnnotations;
|
||||
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
|
||||
|
||||
abstract class Resource extends Primitive
|
||||
{
|
||||
use HasAnnotations;
|
||||
|
||||
protected string $uri = '';
|
||||
|
||||
protected string $mimeType = '';
|
||||
|
||||
protected string $defaultUriScheme = 'file';
|
||||
|
||||
public function uri(): string
|
||||
{
|
||||
if ($this instanceof HasUriTemplate) {
|
||||
return (string) $this->uriTemplate();
|
||||
}
|
||||
|
||||
$attribute = $this->resolveAttribute(Uri::class);
|
||||
|
||||
return $attribute !== null
|
||||
? $attribute->value
|
||||
: ($this->uri !== '' ? $this->uri : $this->defaultUriScheme.'://resources/'.Str::kebab(class_basename($this)));
|
||||
}
|
||||
|
||||
public function mimeType(): string
|
||||
{
|
||||
$attribute = $this->resolveAttribute(MimeType::class);
|
||||
|
||||
return $attribute !== null
|
||||
? $attribute->value
|
||||
: ($this->mimeType !== '' ? $this->mimeType : 'text/plain');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toMethodCall(): array
|
||||
{
|
||||
return ['uri' => $this->uri()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* name: string,
|
||||
* title: string,
|
||||
* description: string,
|
||||
* uri?: string,
|
||||
* uriTemplate?: string,
|
||||
* mimeType: string,
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$annotations = $this->annotations();
|
||||
|
||||
$data = [
|
||||
'name' => $this->name(),
|
||||
'title' => $this->title(),
|
||||
'description' => $this->description(),
|
||||
'mimeType' => $this->mimeType(),
|
||||
];
|
||||
|
||||
if ($annotations !== []) {
|
||||
$data['annotations'] = $annotations;
|
||||
}
|
||||
|
||||
if ($this instanceof HasUriTemplate) {
|
||||
$data['uriTemplate'] = (string) $this->uriTemplate();
|
||||
} else {
|
||||
$data['uri'] = $this->uri();
|
||||
}
|
||||
|
||||
// @phpstan-ignore return.type
|
||||
return $this->mergeMeta($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string>
|
||||
*/
|
||||
protected function allowedAnnotations(): array
|
||||
{
|
||||
return [
|
||||
Annotation::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
109
vendor/laravel/mcp/src/Server/ServerContext.php
vendored
Normal file
109
vendor/laravel/mcp/src/Server/ServerContext.php
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
|
||||
|
||||
class ServerContext
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $supportedProtocolVersions
|
||||
* @param array<string, mixed> $serverCapabilities
|
||||
* @param array<int, Tool|string> $tools
|
||||
* @param array<int, Resource|string> $resources
|
||||
* @param array<int, Prompt|string> $prompts
|
||||
*/
|
||||
public function __construct(
|
||||
public array $supportedProtocolVersions,
|
||||
public array $serverCapabilities,
|
||||
public string $serverName,
|
||||
public string $serverVersion,
|
||||
public string $instructions,
|
||||
public int $maxPaginationLength,
|
||||
public int $defaultPaginationLength,
|
||||
protected array $tools,
|
||||
protected array $resources,
|
||||
protected array $prompts,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Tool>
|
||||
*/
|
||||
public function tools(): Collection
|
||||
{
|
||||
/** @var Collection<int,Tool> $tools */
|
||||
$tools = collect($this->tools);
|
||||
|
||||
return $this->resolvePrimitives($tools);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Resource>
|
||||
*/
|
||||
public function resources(): Collection
|
||||
{
|
||||
/** @var Collection<int,Resource> $resourceTemplates */
|
||||
$resourceTemplates = collect($this->resources)
|
||||
->filter(fn (Resource|string $resource): bool => ! $this->isResourceTemplate($resource));
|
||||
|
||||
return $this->resolvePrimitives($resourceTemplates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, HasUriTemplate&Resource>
|
||||
*/
|
||||
public function resourceTemplates(): Collection
|
||||
{
|
||||
/** @var Collection<int,HasUriTemplate&Resource> $resourceTemplates */
|
||||
$resourceTemplates = collect($this->resources)
|
||||
->filter(fn (Resource|string $resource): bool => $this->isResourceTemplate($resource));
|
||||
|
||||
return $this->resolvePrimitives($resourceTemplates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Prompt>
|
||||
*/
|
||||
public function prompts(): Collection
|
||||
{
|
||||
/** @var Collection<int,Prompt> $prompts */
|
||||
$prompts = collect($this->prompts);
|
||||
|
||||
return $this->resolvePrimitives($prompts);
|
||||
}
|
||||
|
||||
public function perPage(?int $requestedPerPage = null): int
|
||||
{
|
||||
return min($requestedPerPage ?? $this->defaultPaginationLength, $this->maxPaginationLength);
|
||||
}
|
||||
|
||||
public function hasCapability(string $capability): bool
|
||||
{
|
||||
return array_key_exists($capability, $this->serverCapabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of Primitive
|
||||
*
|
||||
* @param Collection<int, T|string> $primitive
|
||||
* @return Collection<int, T>
|
||||
*/
|
||||
private function resolvePrimitives(Collection $primitive): Collection
|
||||
{
|
||||
return $primitive->map(fn (Primitive|string $primitiveClass) => is_string($primitiveClass)
|
||||
? Container::getInstance()->make($primitiveClass)
|
||||
: $primitiveClass)
|
||||
->filter(fn (Primitive $primitive): bool => $primitive->eligibleForRegistration());
|
||||
}
|
||||
|
||||
private function isResourceTemplate(Resource|string $resource): bool
|
||||
{
|
||||
return $resource instanceof HasUriTemplate || (is_string($resource) && is_subclass_of($resource, HasUriTemplate::class));
|
||||
}
|
||||
}
|
||||
175
vendor/laravel/mcp/src/Server/Testing/PendingTestResponse.php
vendored
Normal file
175
vendor/laravel/mcp/src/Server/Testing/PendingTestResponse.php
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Testing;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
|
||||
use Laravel\Mcp\Server\Primitive;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Transport\FakeTransporter;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcRequest;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
|
||||
|
||||
class PendingTestResponse
|
||||
{
|
||||
/**
|
||||
* @param class-string<Server> $serverClass
|
||||
*/
|
||||
public function __construct(
|
||||
protected Container $app,
|
||||
protected string $serverClass
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Tool>|Tool $tool
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public function tool(Tool|string $tool, array $arguments = []): TestResponse
|
||||
{
|
||||
return $this->run('tools/call', $tool, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Prompt>|Prompt $prompt
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public function prompt(Prompt|string $prompt, array $arguments = []): TestResponse
|
||||
{
|
||||
return $this->run('prompts/get', $prompt, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Resource>|Resource $resource
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public function resource(Resource|string $resource, array $arguments = []): TestResponse
|
||||
{
|
||||
return $this->run('resources/read', $resource, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Primitive>|Primitive $primitive
|
||||
* @param array<string, mixed> $currentArgs
|
||||
*/
|
||||
public function completion(
|
||||
Primitive|string $primitive,
|
||||
string $argumentName,
|
||||
string $argumentValue = '',
|
||||
array $currentArgs = []
|
||||
): TestResponse {
|
||||
$primitive = $this->resolvePrimitive($primitive);
|
||||
$server = $this->initializeServer();
|
||||
|
||||
$request = new JsonRpcRequest(
|
||||
uniqid(),
|
||||
'completion/complete',
|
||||
[
|
||||
'ref' => $this->buildCompletionRef($primitive),
|
||||
'argument' => [
|
||||
'name' => $argumentName,
|
||||
'value' => $argumentValue,
|
||||
],
|
||||
'context' => [
|
||||
'arguments' => $currentArgs,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$response = $this->executeRequest($server, $request);
|
||||
|
||||
return new TestResponse($primitive, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function buildCompletionRef(Primitive $primitive): array
|
||||
{
|
||||
return match (true) {
|
||||
$primitive instanceof Prompt => [
|
||||
'type' => 'ref/prompt',
|
||||
'name' => $primitive->name(),
|
||||
],
|
||||
$primitive instanceof Resource => [
|
||||
'type' => 'ref/resource',
|
||||
'uri' => $primitive->uri(),
|
||||
],
|
||||
default => throw new InvalidArgumentException('Unsupported primitive type for completion.'),
|
||||
};
|
||||
}
|
||||
|
||||
protected function resolvePrimitive(Primitive|string $primitive): Primitive
|
||||
{
|
||||
return is_string($primitive)
|
||||
? Container::getInstance()->make($primitive)
|
||||
: $primitive;
|
||||
}
|
||||
|
||||
protected function initializeServer(): Server
|
||||
{
|
||||
$server = Container::getInstance()->make(
|
||||
$this->serverClass,
|
||||
['transport' => new FakeTransporter]
|
||||
);
|
||||
|
||||
$server->start();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
protected function executeRequest(Server $server, JsonRpcRequest $request): mixed
|
||||
{
|
||||
try {
|
||||
return (fn (): iterable|JsonRpcResponse => $this->runMethodHandle($request, $this->createContext()))->call($server);
|
||||
} catch (JsonRpcException $jsonRpcException) {
|
||||
return $jsonRpcException->toJsonRpcResponse();
|
||||
}
|
||||
}
|
||||
|
||||
public function actingAs(Authenticatable $user, ?string $guard = null): static
|
||||
{
|
||||
if (property_exists($user, 'wasRecentlyCreated')) {
|
||||
$user->wasRecentlyCreated = false;
|
||||
}
|
||||
|
||||
$this->app['auth']->guard($guard)->setUser($user);
|
||||
|
||||
$this->app['auth']->shouldUse($guard);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Primitive>|Primitive $primitive
|
||||
* @param array<string, mixed> $arguments
|
||||
*
|
||||
* @throws JsonRpcException
|
||||
*/
|
||||
protected function run(string $method, Primitive|string $primitive, array $arguments = []): TestResponse
|
||||
{
|
||||
$primitive = $this->resolvePrimitive($primitive);
|
||||
$server = $this->initializeServer();
|
||||
|
||||
$request = new JsonRpcRequest(
|
||||
uniqid(),
|
||||
$method,
|
||||
[
|
||||
...$primitive->toMethodCall(),
|
||||
'arguments' => $arguments,
|
||||
],
|
||||
);
|
||||
|
||||
$response = $this->executeRequest($server, $request);
|
||||
|
||||
return new TestResponse($primitive, $response);
|
||||
}
|
||||
}
|
||||
375
vendor/laravel/mcp/src/Server/Testing/TestResponse.php
vendored
Normal file
375
vendor/laravel/mcp/src/Server/Testing/TestResponse.php
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Testing;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
use Illuminate\Support\Traits\Macroable;
|
||||
use Illuminate\Testing\Fluent\AssertableJson;
|
||||
use Laravel\Mcp\Server\Primitive;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Transport\JsonRpcResponse;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use RuntimeException;
|
||||
|
||||
class TestResponse
|
||||
{
|
||||
use Conditionable;
|
||||
use Macroable;
|
||||
|
||||
protected JsonRpcResponse $response;
|
||||
|
||||
/**
|
||||
* @var array<int, JsonRpcResponse>
|
||||
*/
|
||||
protected array $notifications = [];
|
||||
|
||||
/**
|
||||
* @param iterable<int, JsonRpcResponse>|JsonRpcResponse $response
|
||||
*/
|
||||
public function __construct(
|
||||
protected Primitive $primitive,
|
||||
iterable|JsonRpcResponse $response,
|
||||
) {
|
||||
$responses = is_iterable($response)
|
||||
? iterator_to_array($response)
|
||||
: [$response];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
$content = $response->toArray();
|
||||
|
||||
if (isset($content['id'])) {
|
||||
$this->response = $response;
|
||||
} else {
|
||||
$this->notifications[] = $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|string $text
|
||||
*/
|
||||
public function assertSee(array|string $text): static
|
||||
{
|
||||
$seeable = collect([
|
||||
...$this->content(),
|
||||
...$this->errors(),
|
||||
])->filter()->unique()->values()->all();
|
||||
|
||||
foreach (is_array($text) ? $text : [$text] as $segment) {
|
||||
foreach ($seeable as $message) {
|
||||
if (str_contains($message, $segment)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertTrue(false, "The expected text [{$segment}] was not found in the response content.");
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertTrue(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|string $text
|
||||
*/
|
||||
public function assertDontSee(array|string $text): static
|
||||
{
|
||||
$seeable = collect([
|
||||
...$this->content(),
|
||||
...$this->errors(),
|
||||
])->filter()->unique()->values()->all();
|
||||
|
||||
foreach (is_array($text) ? $text : [$text] as $segment) {
|
||||
foreach ($seeable as $message) {
|
||||
if (str_contains($message, $segment)) {
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertTrue(false, "The unexpected text [{$segment}] was found in the response content.");
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertTrue(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|Closure(AssertableJson): bool $structuredContent
|
||||
*/
|
||||
public function assertStructuredContent(Closure|array $structuredContent): static
|
||||
{
|
||||
if ($structuredContent instanceof Closure) {
|
||||
$assertableJson = AssertableJson::fromArray($this->response->toArray()['result']['structuredContent'] ?? null);
|
||||
|
||||
$structuredContent($assertableJson);
|
||||
|
||||
$assertableJson->interacted();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
Assert::assertSame(
|
||||
$structuredContent,
|
||||
$this->response->toArray()['result']['structuredContent'] ?? null,
|
||||
'The expected structured content does not match the actual structured content.'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertNotificationCount(int $count): static
|
||||
{
|
||||
Assert::assertCount($count, $this->notifications, "The expected number of notifications [{$count}] does not match the actual count.");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $params
|
||||
*/
|
||||
public function assertSentNotification(string $method, ?array $params = null): static
|
||||
{
|
||||
foreach ($this->notifications as $notification) {
|
||||
$content = $notification->toArray();
|
||||
|
||||
if ($content['method'] === $method && (is_array($params) === false || $content['params'] === $params)) {
|
||||
Assert::assertTrue(true); // @phpstan-ignore-line
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::fail("The expected notification [{$method}], but it was not found.");
|
||||
}
|
||||
|
||||
public function assertName(string $name): static
|
||||
{
|
||||
Assert::assertEquals(
|
||||
$name,
|
||||
$this->primitive->name(),
|
||||
"The expected name [{$name}] does not match the actual name [{$this->primitive->name()}].",
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertTitle(string $title): static
|
||||
{
|
||||
Assert::assertEquals(
|
||||
$title,
|
||||
$this->primitive->title(),
|
||||
"The expected title [{$title}] does not match the actual title [{$this->primitive->title()}].",
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertDescription(string $description): static
|
||||
{
|
||||
Assert::assertEquals(
|
||||
$description,
|
||||
$this->primitive->description(),
|
||||
"The expected description [{$description}] does not match the actual description [{$this->primitive->description()}].",
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertOk(): static
|
||||
{
|
||||
return $this->assertHasNoErrors();
|
||||
}
|
||||
|
||||
public function assertHasNoErrors(): static
|
||||
{
|
||||
Assert::assertEmpty($this->errors());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $messages
|
||||
*/
|
||||
public function assertHasErrors(array $messages = []): static
|
||||
{
|
||||
$errors = $this->errors();
|
||||
|
||||
Assert::assertNotEmpty($errors, 'The response has no errors.');
|
||||
|
||||
foreach ($messages as $message) {
|
||||
foreach ($errors as $error) {
|
||||
if (str_contains($error, $message)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::fail("The expected error message [{$message}] was not found in the response.");
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertAuthenticated(?string $guard = null): static
|
||||
{
|
||||
Assert::assertTrue($this->isAuthenticated($guard), 'The user is not authenticated');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertGuest(?string $guard = null): static
|
||||
{
|
||||
Assert::assertFalse($this->isAuthenticated($guard), 'The user is authenticated');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertAuthenticatedAs(Authenticatable $user, ?string $guard = null): static
|
||||
{
|
||||
$expected = Container::getInstance()->make('auth')->guard($guard)->user();
|
||||
|
||||
Assert::assertNotNull($expected, 'The current user is not authenticated.');
|
||||
|
||||
Assert::assertInstanceOf(
|
||||
$expected::class, $user,
|
||||
'The currently authenticated user is not who was expected'
|
||||
);
|
||||
|
||||
Assert::assertSame(
|
||||
$expected->getAuthIdentifier(), $user->getAuthIdentifier(),
|
||||
'The currently authenticated user is not who was expected'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function isAuthenticated(?string $guard = null): bool
|
||||
{
|
||||
return Container::getInstance()->make('auth')->guard($guard)->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $expectedValues
|
||||
*/
|
||||
public function assertHasCompletions(array $expectedValues = []): static
|
||||
{
|
||||
$actualValues = $this->completionValues();
|
||||
|
||||
Assert::assertNotNull(
|
||||
$this->response->toArray()['result']['completion'] ?? null,
|
||||
'No completion data found in response.'
|
||||
);
|
||||
|
||||
foreach ($expectedValues as $expected) {
|
||||
Assert::assertContains(
|
||||
$expected,
|
||||
$actualValues,
|
||||
"Expected completion value [{$expected}] not found."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $values
|
||||
*/
|
||||
public function assertCompletionValues(array $values): static
|
||||
{
|
||||
Assert::assertEquals(
|
||||
$values,
|
||||
$this->completionValues(),
|
||||
'Completion values do not match expected values.'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assertCompletionCount(int $count): static
|
||||
{
|
||||
$values = $this->completionValues();
|
||||
|
||||
Assert::assertCount(
|
||||
$count,
|
||||
$values,
|
||||
"Expected {$count} completions, but got ".count($values)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dd(): void
|
||||
{
|
||||
dd($this->response->toArray());
|
||||
}
|
||||
|
||||
public function dump(): void
|
||||
{
|
||||
dump($this->response->toArray());
|
||||
}
|
||||
|
||||
public function ddErrors(): void
|
||||
{
|
||||
dd($this->errors());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function content(): array
|
||||
{
|
||||
return (match (true) {
|
||||
// @phpstan-ignore-next-line
|
||||
$this->primitive instanceof Tool => collect($this->response->toArray()['result']['content'] ?? [])
|
||||
->map(fn (array $message): string => $message['text'] ?? $message['data'] ?? ''),
|
||||
// @phpstan-ignore-next-line
|
||||
$this->primitive instanceof Prompt => collect($this->response->toArray()['result']['messages'] ?? [])
|
||||
->map(fn (array $message): array => $message['content'])
|
||||
->map(fn (array $content): string => $content['text'] ?? $content['data'] ?? ''),
|
||||
// @phpstan-ignore-next-line
|
||||
$this->primitive instanceof Resource => collect($this->response->toArray()['result']['contents'] ?? [])
|
||||
->map(fn (array $item): string => $item['text'] ?? $item['blob'] ?? ''),
|
||||
default => throw new RuntimeException('This primitive type is not supported.'),
|
||||
})->filter()->unique()->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function errors(): array
|
||||
{
|
||||
$response = $this->response->toArray();
|
||||
|
||||
if (data_get($response, 'result.isError', false)) {
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
if (array_key_exists('error', $response)) {
|
||||
return [$response['error']['message']];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected function completionValues(): array
|
||||
{
|
||||
$response = $this->response->toArray();
|
||||
|
||||
return $response['result']['completion']['values'] ?? [];
|
||||
}
|
||||
}
|
||||
109
vendor/laravel/mcp/src/Server/Tool.php
vendored
Normal file
109
vendor/laravel/mcp/src/Server/Tool.php
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\JsonSchema\JsonSchema as JsonSchemaFactory;
|
||||
use Laravel\Mcp\Server\Attributes\RendersApp;
|
||||
use Laravel\Mcp\Server\Concerns\HasAnnotations;
|
||||
use Laravel\Mcp\Server\Tools\Annotations\ToolAnnotation;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Visibility;
|
||||
|
||||
abstract class Tool extends Primitive
|
||||
{
|
||||
use HasAnnotations;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the output schema for this tool's results.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function outputSchema(JsonSchema $schema): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toMethodCall(): array
|
||||
{
|
||||
return ['name' => $this->name()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's array representation.
|
||||
*
|
||||
* @return array{
|
||||
* name: string,
|
||||
* title?: string|null,
|
||||
* description?: string|null,
|
||||
* inputSchema?: array<string, mixed>,
|
||||
* outputSchema?: array<string, mixed>,
|
||||
* annotations?: array<string, mixed>|object,
|
||||
* _meta?: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$annotations = $this->annotations();
|
||||
|
||||
$schema = JsonSchemaFactory::object(
|
||||
$this->schema(...),
|
||||
)->toArray();
|
||||
|
||||
$outputSchema = JsonSchemaFactory::object(
|
||||
$this->outputSchema(...),
|
||||
)->toArray();
|
||||
|
||||
$schema['properties'] ??= (object) [];
|
||||
|
||||
$result = [
|
||||
'name' => $this->name(),
|
||||
'title' => $this->title(),
|
||||
'description' => $this->description(),
|
||||
'inputSchema' => $schema,
|
||||
'annotations' => $annotations === [] ? (object) [] : $annotations,
|
||||
];
|
||||
|
||||
if (isset($outputSchema['properties'])) {
|
||||
$result['outputSchema'] = $outputSchema;
|
||||
}
|
||||
|
||||
$rendersApp = $this->resolveAttribute(RendersApp::class);
|
||||
|
||||
if ($rendersApp !== null) {
|
||||
/** @var AppResource $appResource */
|
||||
$appResource = Container::getInstance()->make($rendersApp->resource);
|
||||
|
||||
$this->setMeta('ui', [
|
||||
'resourceUri' => $appResource->uri(),
|
||||
'visibility' => array_map(fn (Visibility $visiblity) => $visiblity->value, $rendersApp->visibility),
|
||||
]);
|
||||
}
|
||||
|
||||
// @phpstan-ignore return.type
|
||||
return $this->mergeMeta($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string>
|
||||
*/
|
||||
protected function allowedAnnotations(): array
|
||||
{
|
||||
return [
|
||||
ToolAnnotation::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsDestructive.php
vendored
Normal file
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsDestructive.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Tools\Annotations;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class IsDestructive extends ToolAnnotation
|
||||
{
|
||||
public function __construct(public bool $value = true)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'destructiveHint';
|
||||
}
|
||||
}
|
||||
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsIdempotent.php
vendored
Normal file
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsIdempotent.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Tools\Annotations;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class IsIdempotent extends ToolAnnotation
|
||||
{
|
||||
public function __construct(public bool $value = true)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'idempotentHint';
|
||||
}
|
||||
}
|
||||
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsOpenWorld.php
vendored
Normal file
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsOpenWorld.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Tools\Annotations;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class IsOpenWorld extends ToolAnnotation
|
||||
{
|
||||
public function __construct(public bool $value = true)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'openWorldHint';
|
||||
}
|
||||
}
|
||||
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsReadOnly.php
vendored
Normal file
21
vendor/laravel/mcp/src/Server/Tools/Annotations/IsReadOnly.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Tools\Annotations;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class IsReadOnly extends ToolAnnotation
|
||||
{
|
||||
public function __construct(public bool $value = true)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return 'readOnlyHint';
|
||||
}
|
||||
}
|
||||
12
vendor/laravel/mcp/src/Server/Tools/Annotations/ToolAnnotation.php
vendored
Normal file
12
vendor/laravel/mcp/src/Server/Tools/Annotations/ToolAnnotation.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Tools\Annotations;
|
||||
|
||||
use Laravel\Mcp\Server\Contracts\Annotation;
|
||||
|
||||
abstract class ToolAnnotation implements Annotation
|
||||
{
|
||||
//
|
||||
}
|
||||
39
vendor/laravel/mcp/src/Server/Transport/FakeTransporter.php
vendored
Normal file
39
vendor/laravel/mcp/src/Server/Transport/FakeTransporter.php
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Response;
|
||||
use Laravel\Mcp\Server\Contracts\Transport;
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class FakeTransporter implements Transport
|
||||
{
|
||||
public function onReceive(Closure $handler): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function send(string $message, ?string $sessionId = null): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function run(): Response|StreamedResponse
|
||||
{
|
||||
throw new LogicException('Not implemented.');
|
||||
}
|
||||
|
||||
public function sessionId(): ?string
|
||||
{
|
||||
return uniqid();
|
||||
}
|
||||
|
||||
public function stream(Closure $stream): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
126
vendor/laravel/mcp/src/Server/Transport/HttpTransport.php
vendored
Normal file
126
vendor/laravel/mcp/src/Server/Transport/HttpTransport.php
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Laravel\Mcp\Server\Contracts\Transport;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class HttpTransport implements Transport
|
||||
{
|
||||
/**
|
||||
* @param (Closure(string): void)|null $handler
|
||||
*/
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected string $sessionId,
|
||||
protected ?Closure $handler = null,
|
||||
protected ?string $reply = null,
|
||||
protected ?string $replySessionId = null,
|
||||
protected ?Closure $stream = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public function onReceive(Closure $handler): void
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
public function send(string $message, ?string $sessionId = null): void
|
||||
{
|
||||
if ($this->stream instanceof Closure) {
|
||||
$this->sendStreamMessage($message);
|
||||
}
|
||||
|
||||
$this->reply = $message;
|
||||
$this->replySessionId = $sessionId;
|
||||
}
|
||||
|
||||
public function run(): Response|StreamedResponse
|
||||
{
|
||||
if (is_callable($this->handler)) {
|
||||
($this->handler)($this->request->getContent());
|
||||
}
|
||||
|
||||
if ($this->stream instanceof Closure) {
|
||||
$stream = $this->stream;
|
||||
|
||||
return response()->stream(function () use ($stream): void {
|
||||
$result = $stream();
|
||||
|
||||
if (! is_iterable($result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($result as $message) {
|
||||
if (connection_aborted() !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendStreamMessage((string) $message);
|
||||
}
|
||||
}, 200, $this->getHeaders());
|
||||
}
|
||||
|
||||
// Must be 202 - https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server
|
||||
$statusCode = $this->reply === null ? 202 : 200;
|
||||
$response = response($this->reply, $statusCode, $this->getHeaders());
|
||||
|
||||
assert($response instanceof Response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function sessionId(): ?string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a streaming callback.
|
||||
*
|
||||
* The callback may echo SSE-formatted output directly or return an iterable of message payloads.
|
||||
*
|
||||
* @param Closure(): (iterable<string>|void) $stream
|
||||
*/
|
||||
public function stream(Closure $stream): void
|
||||
{
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
protected function sendStreamMessage(string $message): void
|
||||
{
|
||||
echo 'data: '.$message."\n\n";
|
||||
|
||||
if (ob_get_level() !== 0) {
|
||||
ob_flush();
|
||||
}
|
||||
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getHeaders(): array
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type' => $this->stream instanceof Closure ? 'text/event-stream' : 'application/json',
|
||||
];
|
||||
|
||||
if ($this->replySessionId !== null) {
|
||||
$headers['MCP-Session-Id'] = $this->replySessionId;
|
||||
}
|
||||
|
||||
if ($this->stream instanceof Closure) {
|
||||
$headers['X-Accel-Buffering'] = 'no';
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
41
vendor/laravel/mcp/src/Server/Transport/JsonRpcNotification.php
vendored
Normal file
41
vendor/laravel/mcp/src/Server/Transport/JsonRpcNotification.php
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
|
||||
|
||||
class JsonRpcNotification
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function __construct(
|
||||
public string $method,
|
||||
public array $params,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{jsonrpc?: mixed, method?: mixed, params?: array<string, mixed>} $jsonRequest
|
||||
*
|
||||
* @throws JsonRpcException
|
||||
*/
|
||||
public static function from(array $jsonRequest): static
|
||||
{
|
||||
if (! isset($jsonRequest['jsonrpc']) || $jsonRequest['jsonrpc'] !== '2.0') {
|
||||
throw new JsonRpcException('Invalid Request: Invalid JSON-RPC version. Must be "2.0".', -32600);
|
||||
}
|
||||
|
||||
if (! isset($jsonRequest['method']) || ! is_string($jsonRequest['method'])) {
|
||||
throw new JsonRpcException('Invalid Request: Invalid or missing "method". Must be a string.', -32600);
|
||||
}
|
||||
|
||||
return new static(
|
||||
method: $jsonRequest['method'],
|
||||
params: $jsonRequest['params'] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
75
vendor/laravel/mcp/src/Server/Transport/JsonRpcRequest.php
vendored
Normal file
75
vendor/laravel/mcp/src/Server/Transport/JsonRpcRequest.php
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Server\Exceptions\JsonRpcException;
|
||||
|
||||
class JsonRpcRequest
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function __construct(
|
||||
public int|string $id,
|
||||
public string $method,
|
||||
public array $params,
|
||||
public ?string $sessionId = null
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id: mixed, jsonrpc?: mixed, method?: mixed, params?: array<string, mixed>} $jsonRequest
|
||||
*
|
||||
* @throws JsonRpcException
|
||||
*/
|
||||
public static function from(array $jsonRequest, ?string $sessionId = null): static
|
||||
{
|
||||
$requestId = $jsonRequest['id'];
|
||||
|
||||
if (! is_int($jsonRequest['id']) && ! is_string($jsonRequest['id'])) {
|
||||
throw new JsonRpcException('Invalid Request: The [id] member must be a string, number.', -32600, $requestId);
|
||||
}
|
||||
|
||||
if (! isset($jsonRequest['jsonrpc']) || $jsonRequest['jsonrpc'] !== '2.0') {
|
||||
throw new JsonRpcException('Invalid Request: The [jsonrpc] member must be exactly [2.0].', -32600, $requestId);
|
||||
}
|
||||
|
||||
if (! isset($jsonRequest['method']) || ! is_string($jsonRequest['method'])) {
|
||||
throw new JsonRpcException('Invalid Request: The [method] member is required and must be a string.', -32600, $requestId);
|
||||
}
|
||||
|
||||
return new static(
|
||||
id: $requestId,
|
||||
method: $jsonRequest['method'],
|
||||
params: $jsonRequest['params'] ?? [],
|
||||
sessionId: $sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
public function cursor(): ?string
|
||||
{
|
||||
return $this->get('cursor');
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->params[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function meta(): ?array
|
||||
{
|
||||
return isset($this->params['_meta']) && is_array($this->params['_meta']) ? $this->params['_meta'] : null;
|
||||
}
|
||||
|
||||
public function toRequest(): Request
|
||||
{
|
||||
return new Request($this->params['arguments'] ?? [], $this->sessionId, $this->meta());
|
||||
}
|
||||
}
|
||||
76
vendor/laravel/mcp/src/Server/Transport/JsonRpcResponse.php
vendored
Normal file
76
vendor/laravel/mcp/src/Server/Transport/JsonRpcResponse.php
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class JsonRpcResponse implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $content
|
||||
*/
|
||||
public function __construct(protected array $content = []) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $result
|
||||
*/
|
||||
public static function result(int|string $id, array $result): static
|
||||
{
|
||||
return new static([
|
||||
'id' => $id,
|
||||
'result' => $result === [] ? (object) [] : $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function notification(string $method, array $params): static
|
||||
{
|
||||
return new static([
|
||||
'method' => $method,
|
||||
'params' => $params === [] ? (object) [] : $params,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public static function error(string|int|null $id, int $code, string $message, ?array $data = null): static
|
||||
{
|
||||
$error = [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$error['data'] = $data;
|
||||
}
|
||||
|
||||
return new static([
|
||||
...$id === null ? [] : ['id' => $id],
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'jsonrpc' => '2.0',
|
||||
...$this->content,
|
||||
];
|
||||
}
|
||||
|
||||
public function toJson(int $options = 0): string
|
||||
{
|
||||
return json_encode($this->toArray(), $options | JSON_UNESCAPED_UNICODE) ?: '';
|
||||
}
|
||||
}
|
||||
58
vendor/laravel/mcp/src/Server/Transport/StdioTransport.php
vendored
Normal file
58
vendor/laravel/mcp/src/Server/Transport/StdioTransport.php
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Transport;
|
||||
|
||||
use Closure;
|
||||
use Laravel\Mcp\Server\Contracts\Transport;
|
||||
|
||||
class StdioTransport implements Transport
|
||||
{
|
||||
/**
|
||||
* @param (Closure(string): void)|null $handler
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $sessionId,
|
||||
protected ?Closure $handler = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public function onReceive(Closure $handler): void
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
public function send(string $message, ?string $sessionId = null): void
|
||||
{
|
||||
fwrite(STDOUT, $message.PHP_EOL);
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
stream_set_blocking(STDIN, false);
|
||||
|
||||
while (! feof(STDIN)) {
|
||||
if (($line = fgets(STDIN)) === false) {
|
||||
usleep(10000);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_callable($this->handler)) {
|
||||
($this->handler)($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function sessionId(): string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
public function stream(Closure $stream): void
|
||||
{
|
||||
$stream();
|
||||
}
|
||||
}
|
||||
105
vendor/laravel/mcp/src/Server/Ui/AppMeta.php
vendored
Normal file
105
vendor/laravel/mcp/src/Server/Ui/AppMeta.php
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Library;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class AppMeta implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param array<int, Library> $libraries
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?Csp $csp = null,
|
||||
protected ?Permissions $permissions = null,
|
||||
protected ?string $domain = null,
|
||||
protected ?bool $prefersBorder = true,
|
||||
protected array $libraries = [],
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public static function make(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
public function csp(Csp $csp): static
|
||||
{
|
||||
$this->csp = $csp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function permissions(Permissions $permissions): static
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function domain(string $domain): static
|
||||
{
|
||||
$this->domain = $domain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function prefersBorder(bool $prefersBorder = true): static
|
||||
{
|
||||
$this->prefersBorder = $prefersBorder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function libraries(Library ...$libraries): static
|
||||
{
|
||||
$this->libraries = array_values($libraries);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Library>
|
||||
*/
|
||||
public function getLibraries(): array
|
||||
{
|
||||
return $this->libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$cspArray = $this->csp?->toArray() ?: [];
|
||||
|
||||
if ($this->libraries !== []) {
|
||||
$libraryDomains = collect($this->libraries)
|
||||
->map(fn (Library $lib): array => $lib->domains())
|
||||
->flatten();
|
||||
|
||||
/** @var array<int, string> $existingDomains */
|
||||
$existingDomains = $cspArray['resourceDomains'] ?? [];
|
||||
|
||||
$cspArray['resourceDomains'] = collect($existingDomains)
|
||||
->merge($libraryDomains)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
return array_filter([
|
||||
'csp' => $cspArray ?: null,
|
||||
'permissions' => $this->permissions?->toArray() ?: null,
|
||||
'domain' => $this->domain,
|
||||
'prefersBorder' => $this->prefersBorder,
|
||||
], fn (mixed $value): bool => $value !== null);
|
||||
}
|
||||
}
|
||||
86
vendor/laravel/mcp/src/Server/Ui/Csp.php
vendored
Normal file
86
vendor/laravel/mcp/src/Server/Ui/Csp.php
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class Csp implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param array<int, string>|null $connectDomains Domains the app may connect to via fetch, XHR, or WebSocket (CSP connect-src).
|
||||
* @param array<int, string>|null $resourceDomains Domains the app may load images, scripts, styles, and fonts from (CSP default-src).
|
||||
* @param array<int, string>|null $frameDomains Domains the app may embed as nested iframes (CSP frame-src).
|
||||
* @param array<int, string>|null $baseUriDomains Allowed URLs for the document's base element (CSP base-uri).
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?array $connectDomains = null,
|
||||
protected ?array $resourceDomains = null,
|
||||
protected ?array $frameDomains = null,
|
||||
protected ?array $baseUriDomains = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
public static function make(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $domains
|
||||
*/
|
||||
public function connectDomains(array $domains): static
|
||||
{
|
||||
$this->connectDomains = $domains;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $domains
|
||||
*/
|
||||
public function resourceDomains(array $domains): static
|
||||
{
|
||||
$this->resourceDomains = $domains;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $domains
|
||||
*/
|
||||
public function frameDomains(array $domains): static
|
||||
{
|
||||
$this->frameDomains = $domains;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $domains
|
||||
*/
|
||||
public function baseUriDomains(array $domains): static
|
||||
{
|
||||
$this->baseUriDomains = $domains;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_filter([
|
||||
'connectDomains' => $this->connectDomains,
|
||||
'resourceDomains' => $this->resourceDomains,
|
||||
'frameDomains' => $this->frameDomains,
|
||||
'baseUriDomains' => $this->baseUriDomains,
|
||||
], fn (mixed $value): bool => $value !== null && $value !== []);
|
||||
}
|
||||
}
|
||||
43
vendor/laravel/mcp/src/Server/Ui/Enums/Library.php
vendored
Normal file
43
vendor/laravel/mcp/src/Server/Ui/Enums/Library.php
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui\Enums;
|
||||
|
||||
enum Library: string
|
||||
{
|
||||
case Tailwind = 'tailwind';
|
||||
case Alpine = 'alpine';
|
||||
|
||||
/**
|
||||
* CSP resource domains required by this library.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function domains(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Tailwind => ['https://cdn.tailwindcss.com'],
|
||||
self::Alpine => ['https://cdn.jsdelivr.net'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML script tag to include in the document head.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function scriptTags(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Tailwind => [
|
||||
'<script src="https://cdn.tailwindcss.com"></script>',
|
||||
"<script>tailwind.config = { darkMode: ['selector', '[data-theme=\"dark\"]'] }</script>",
|
||||
],
|
||||
self::Alpine => [
|
||||
'<style>[x-cloak] { display: none !important; }</style>',
|
||||
'<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
13
vendor/laravel/mcp/src/Server/Ui/Enums/Permission.php
vendored
Normal file
13
vendor/laravel/mcp/src/Server/Ui/Enums/Permission.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui\Enums;
|
||||
|
||||
enum Permission: string
|
||||
{
|
||||
case Camera = 'camera';
|
||||
case Microphone = 'microphone';
|
||||
case Geolocation = 'geolocation';
|
||||
case ClipboardWrite = 'clipboardWrite';
|
||||
}
|
||||
11
vendor/laravel/mcp/src/Server/Ui/Enums/Visibility.php
vendored
Normal file
11
vendor/laravel/mcp/src/Server/Ui/Enums/Visibility.php
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui\Enums;
|
||||
|
||||
enum Visibility: string
|
||||
{
|
||||
case Model = 'model';
|
||||
case App = 'app';
|
||||
}
|
||||
64
vendor/laravel/mcp/src/Server/Ui/Permissions.php
vendored
Normal file
64
vendor/laravel/mcp/src/Server/Ui/Permissions.php
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Server\Ui;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Laravel\Mcp\Server\Ui\Enums\Permission;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, mixed>
|
||||
*/
|
||||
class Permissions implements Arrayable
|
||||
{
|
||||
/** @var array<int, Permission> */
|
||||
protected array $enabled = [];
|
||||
|
||||
public static function make(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
public function camera(): static
|
||||
{
|
||||
return $this->allow(Permission::Camera);
|
||||
}
|
||||
|
||||
public function microphone(): static
|
||||
{
|
||||
return $this->allow(Permission::Microphone);
|
||||
}
|
||||
|
||||
public function geolocation(): static
|
||||
{
|
||||
return $this->allow(Permission::Geolocation);
|
||||
}
|
||||
|
||||
public function clipboardWrite(): static
|
||||
{
|
||||
return $this->allow(Permission::ClipboardWrite);
|
||||
}
|
||||
|
||||
public function allow(Permission ...$permissions): static
|
||||
{
|
||||
array_push($this->enabled, ...$permissions);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, stdClass>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->enabled as $permission) {
|
||||
$result[$permission->value] = new stdClass;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
133
vendor/laravel/mcp/src/Support/UriTemplate.php
vendored
Normal file
133
vendor/laravel/mcp/src/Support/UriTemplate.php
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Support;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
class UriTemplate implements Stringable
|
||||
{
|
||||
private const MAX_TEMPLATE_LENGTH = 1_000_000;
|
||||
|
||||
private const MAX_VARIABLE_LENGTH = 1_000_000;
|
||||
|
||||
private const MAX_TEMPLATE_EXPRESSIONS = 10_000;
|
||||
|
||||
private const MAX_REGEX_LENGTH = 1_000_000;
|
||||
|
||||
private const URI_TEMPLATE_PATTERN = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/.*{[^{}]+}.*/';
|
||||
|
||||
/** @var list<string> */
|
||||
private array $variableNames = [];
|
||||
|
||||
private ?string $compiledRegex = null;
|
||||
|
||||
public function __construct(private readonly string $template)
|
||||
{
|
||||
$this->validateLength($template, self::MAX_TEMPLATE_LENGTH, 'Template');
|
||||
|
||||
if (! preg_match(self::URI_TEMPLATE_PATTERN, $template)) {
|
||||
throw new InvalidArgumentException('Invalid URI template: must be a valid URI template with at least one placeholder.');
|
||||
}
|
||||
|
||||
$this->variableNames = $this->extractVariableNames($template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
public function match(string $uri): ?array
|
||||
{
|
||||
$this->validateLength($uri, self::MAX_TEMPLATE_LENGTH, 'URI');
|
||||
|
||||
$this->compiledRegex ??= $this->compileRegex();
|
||||
|
||||
if (! preg_match($this->compiledRegex, $uri, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->variableNames as $i => $name) {
|
||||
$result[$name] = $matches[$i + 1] ?? '';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
private function validateLength(string $str, int $max, string $context): void
|
||||
{
|
||||
throw_if(
|
||||
Str::length($str) > $max,
|
||||
InvalidArgumentException::class,
|
||||
sprintf('%s exceeds the maximum length of %d characters (received %d)', $context, $max, Str::length($str))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function extractVariableNames(string $template): array
|
||||
{
|
||||
$expressionCount = 0;
|
||||
$names = [];
|
||||
|
||||
if (! preg_match_all('/\{(\w+)}/', $template, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($matches[1] as $name) {
|
||||
$expressionCount++;
|
||||
|
||||
throw_if(
|
||||
$expressionCount > self::MAX_TEMPLATE_EXPRESSIONS,
|
||||
InvalidArgumentException::class,
|
||||
sprintf('Template contains too many expressions (max %d)', self::MAX_TEMPLATE_EXPRESSIONS)
|
||||
);
|
||||
|
||||
$this->validateLength($name, self::MAX_VARIABLE_LENGTH, 'Variable name');
|
||||
$names[] = $name;
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
private function compileRegex(): string
|
||||
{
|
||||
$regexParts = [];
|
||||
|
||||
$segments = preg_split('/(\{\w+})/', $this->template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
throw_unless(
|
||||
$segments,
|
||||
InvalidArgumentException::class,
|
||||
'Failed to compile URI template regex: preg_split error'
|
||||
);
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
$isVariable = preg_match('/^\{(\w+)}$/', $segment);
|
||||
|
||||
throw_if(
|
||||
$isVariable === false,
|
||||
InvalidArgumentException::class,
|
||||
'Failed to validate template segment: preg_match error'
|
||||
);
|
||||
|
||||
$regexParts[] = $isVariable === 1 ? '([^/]+)' : preg_quote($segment, '#');
|
||||
}
|
||||
|
||||
$pattern = '#^'.implode('', $regexParts).'$#';
|
||||
|
||||
$this->validateLength($pattern, self::MAX_REGEX_LENGTH, 'Generated regex pattern');
|
||||
|
||||
return $pattern;
|
||||
}
|
||||
}
|
||||
24
vendor/laravel/mcp/src/Support/ValidationMessages.php
vendored
Normal file
24
vendor/laravel/mcp/src/Support/ValidationMessages.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Laravel\Mcp\Support;
|
||||
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ValidationMessages
|
||||
{
|
||||
public static function from(ValidationException $exception): string
|
||||
{
|
||||
$messages = collect($exception->errors())->flatten()->all();
|
||||
|
||||
if (count($messages) === 0 || ! is_string($messages[0])) {
|
||||
$translator = Validator::getTranslator();
|
||||
|
||||
return $translator->get('The given data was invalid.');
|
||||
}
|
||||
|
||||
return implode(' ', $messages);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user