php-agentic/View/Modal/Admin/Playground.php
Snider adcc163775 refactor: update namespaces for L1/L2 package convention
- Core\Mod\Tenant -> Core\Tenant
- Core\Service\Agentic -> Core\Mod\Agentic\Service

Part of namespace restructure to align with L1/L2 module conventions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:34:46 +00:00

263 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\View\Modal\Admin;
use Core\Api\Models\ApiKey;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
/**
* MCP Playground - interactive tool testing in the browser.
*/
#[Layout('hub::admin.layouts.app')]
class Playground extends Component
{
public string $selectedServer = '';
public string $selectedTool = '';
public array $arguments = [];
public string $response = '';
public bool $loading = false;
public string $apiKey = '';
public ?string $error = null;
public ?string $keyStatus = null;
public ?array $keyInfo = null;
public array $servers = [];
public array $tools = [];
public ?array $toolSchema = null;
public function mount(): void
{
$this->loadServers();
}
public function loadServers(): void
{
try {
$registry = $this->loadRegistry();
$this->servers = collect($registry['servers'] ?? [])
->map(fn ($ref) => $this->loadServerSummary($ref['id']))
->filter()
->values()
->toArray();
} catch (\Throwable $e) {
$this->error = 'Failed to load servers';
$this->servers = [];
}
}
public function updatedSelectedServer(): void
{
$this->error = null;
$this->selectedTool = '';
$this->toolSchema = null;
$this->arguments = [];
$this->response = '';
if (! $this->selectedServer) {
$this->tools = [];
return;
}
try {
$server = $this->loadServerFull($this->selectedServer);
$this->tools = $server['tools'] ?? [];
} catch (\Throwable $e) {
$this->error = 'Failed to load server tools';
$this->tools = [];
}
}
public function updatedSelectedTool(): void
{
$this->error = null;
$this->arguments = [];
$this->response = '';
if (! $this->selectedTool) {
$this->toolSchema = null;
return;
}
try {
$this->toolSchema = collect($this->tools)->firstWhere('name', $this->selectedTool);
// Pre-fill arguments with defaults
$params = $this->toolSchema['inputSchema']['properties'] ?? [];
foreach ($params as $name => $schema) {
$this->arguments[$name] = $schema['default'] ?? '';
}
} catch (\Throwable $e) {
$this->error = 'Failed to load tool schema';
$this->toolSchema = null;
}
}
public function updatedApiKey(): void
{
// Clear key status when key changes
$this->keyStatus = null;
$this->keyInfo = null;
}
public function validateKey(): void
{
$this->keyStatus = null;
$this->keyInfo = null;
if (empty($this->apiKey)) {
$this->keyStatus = 'empty';
return;
}
$key = ApiKey::findByPlainKey($this->apiKey);
if (! $key) {
$this->keyStatus = 'invalid';
return;
}
if ($key->isExpired()) {
$this->keyStatus = 'expired';
return;
}
$this->keyStatus = 'valid';
$this->keyInfo = [
'name' => $key->name,
'scopes' => $key->scopes,
'server_scopes' => $key->getAllowedServers(),
'workspace' => $key->workspace?->name ?? 'Unknown',
'last_used' => $key->last_used_at?->diffForHumans() ?? 'Never',
];
}
public function isAuthenticated(): bool
{
return auth()->check();
}
public function execute(): void
{
if (! $this->selectedServer || ! $this->selectedTool) {
return;
}
$this->loading = true;
$this->response = '';
$this->error = null;
try {
// Filter out empty arguments
$args = array_filter($this->arguments, fn ($v) => $v !== '' && $v !== null);
// Convert numeric strings to numbers where appropriate
foreach ($args as $key => $value) {
if (is_numeric($value)) {
$args[$key] = str_contains($value, '.') ? (float) $value : (int) $value;
}
if ($value === 'true') {
$args[$key] = true;
}
if ($value === 'false') {
$args[$key] = false;
}
}
$payload = [
'server' => $this->selectedServer,
'tool' => $this->selectedTool,
'arguments' => $args,
];
// If we have an API key, make a real request
if (! empty($this->apiKey) && $this->keyStatus === 'valid') {
$response = Http::withToken($this->apiKey)
->timeout(30)
->post(config('app.url').'/api/v1/mcp/tools/call', $payload);
$this->response = json_encode([
'status' => $response->status(),
'response' => $response->json(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return;
}
// Otherwise, just show request format
$this->response = json_encode([
'request' => $payload,
'note' => 'Add an API key above to execute this request live.',
'curl' => sprintf(
"curl -X POST %s/api/v1/mcp/tools/call \\\n -H \"Authorization: Bearer YOUR_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '%s'",
config('app.url'),
json_encode($payload, JSON_UNESCAPED_SLASHES)
),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (\Throwable $e) {
$this->response = json_encode([
'error' => $e->getMessage(),
], JSON_PRETTY_PRINT);
} finally {
$this->loading = false;
}
}
public function render()
{
$isAuthenticated = $this->isAuthenticated();
$workspace = $isAuthenticated ? auth()->user()?->defaultHostWorkspace() : null;
return view('mcp::admin.playground', [
'isAuthenticated' => $isAuthenticated,
'workspace' => $workspace,
]);
}
protected function loadRegistry(): array
{
$path = resource_path('mcp/registry.yaml');
return file_exists($path) ? Yaml::parseFile($path) : ['servers' => []];
}
protected function loadServerFull(string $id): ?array
{
$path = resource_path("mcp/servers/{$id}.yaml");
return file_exists($path) ? Yaml::parseFile($path) : null;
}
protected function loadServerSummary(string $id): ?array
{
$server = $this->loadServerFull($id);
if (! $server) {
return null;
}
return [
'id' => $server['id'],
'name' => $server['name'],
'tagline' => $server['tagline'] ?? '',
];
}
}