feat: absorb Front\Mcp frontage from php-framework

Move the MCP frontage ServiceProvider (Core\Front\Mcp\Boot), McpContext
DTO, McpToolHandler interface, and MCP portal blade layout from
php-framework into this package. Namespaces unchanged — added PSR-4
mapping for Core\Front\Mcp\ and auto-discovery provider.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-04 13:05:16 +00:00
parent be85428d4c
commit cd82959a0e
5 changed files with 323 additions and 2 deletions

View file

@ -16,7 +16,8 @@
"autoload": {
"psr-4": {
"Core\\Mcp\\": "src/Mcp/",
"Core\\Website\\Mcp\\": "src/Website/Mcp/"
"Core\\Website\\Mcp\\": "src/Website/Mcp/",
"Core\\Front\\Mcp\\": "src/Front/Mcp/"
}
},
"autoload-dev": {
@ -26,7 +27,9 @@
},
"extra": {
"laravel": {
"providers": []
"providers": [
"Core\\Front\\Mcp\\Boot"
]
}
},
"minimum-stability": "stable",

50
src/Front/Mcp/Boot.php Normal file
View file

@ -0,0 +1,50 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp;
use Core\LifecycleEventProvider;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\ServiceProvider;
/**
* MCP frontage - MCP API stage.
*
* Provides mcp middleware group for MCP protocol routes.
* Authentication middleware should be added by the core-mcp package.
*/
class Boot extends ServiceProvider
{
/**
* Configure mcp middleware group.
*/
public static function middleware(Middleware $middleware): void
{
$middleware->group('mcp', [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
}
public function register(): void
{
//
}
public function boot(): void
{
// Fire McpRoutesRegistering event for lazy-loaded modules
LifecycleEventProvider::fireMcpRoutes();
// Fire McpToolsRegistering so modules can register tool handlers
LifecycleEventProvider::fireMcpTools();
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp\Contracts;
use Core\Front\Mcp\McpContext;
/**
* Interface for MCP tool handlers.
*
* Each MCP tool is implemented as a handler class that provides:
* - A JSON schema describing the tool for Claude
* - A handle method that processes tool invocations
*
* Tool handlers are registered via the McpToolsRegistering event
* and can be used by both stdio and HTTP MCP transports.
*/
interface McpToolHandler
{
/**
* Get the JSON schema describing this tool.
*
* The schema follows the MCP tool specification:
* - name: Tool identifier (snake_case)
* - description: What the tool does (for Claude)
* - inputSchema: JSON Schema for parameters
*
* @return array{name: string, description: string, inputSchema: array}
*/
public static function schema(): array;
/**
* Handle a tool invocation.
*
* @param array $args Arguments from the tool call
* @param McpContext $context Server context (session, notifications, etc.)
* @return array Result to return to Claude
*/
public function handle(array $args, McpContext $context): array;
}

View file

@ -0,0 +1,133 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp;
use Closure;
/**
* Context object passed to MCP tool handlers.
*
* Abstracts the transport layer (stdio vs HTTP) so tool handlers
* can work with either transport without modification.
*
* Provides access to:
* - Current session tracking
* - Current plan context
* - Notification sending
* - Session logging
*/
class McpContext
{
/**
* @param object|null $currentPlan AgentPlan model instance when Agentic module installed
*/
public function __construct(
private ?string $sessionId = null,
private ?object $currentPlan = null,
private ?Closure $notificationCallback = null,
private ?Closure $logCallback = null,
) {}
/**
* Get the current session ID if one is active.
*/
public function getSessionId(): ?string
{
return $this->sessionId;
}
/**
* Set the current session ID.
*/
public function setSessionId(?string $sessionId): void
{
$this->sessionId = $sessionId;
}
/**
* Get the current plan if one is active.
*
* @return object|null AgentPlan model instance when Agentic module installed
*/
public function getCurrentPlan(): ?object
{
return $this->currentPlan;
}
/**
* Set the current plan.
*
* @param object|null $plan AgentPlan model instance
*/
public function setCurrentPlan(?object $plan): void
{
$this->currentPlan = $plan;
}
/**
* Send an MCP notification to the client.
*
* Notifications are one-way messages that don't expect a response.
* Common notifications include progress updates, log messages, etc.
*/
public function sendNotification(string $method, array $params = []): void
{
if ($this->notificationCallback) {
($this->notificationCallback)($method, $params);
}
}
/**
* Log a message to the current session.
*
* Messages are recorded in the session log for handoff context
* and audit trail purposes.
*/
public function logToSession(string $message, string $type = 'info', array $data = []): void
{
if ($this->logCallback) {
($this->logCallback)($message, $type, $data);
}
}
/**
* Set the notification callback.
*/
public function setNotificationCallback(?Closure $callback): void
{
$this->notificationCallback = $callback;
}
/**
* Set the log callback.
*/
public function setLogCallback(?Closure $callback): void
{
$this->logCallback = $callback;
}
/**
* Check if a session is currently active.
*/
public function hasSession(): bool
{
return $this->sessionId !== null;
}
/**
* Check if a plan is currently active.
*/
public function hasPlan(): bool
{
return $this->currentPlan !== null;
}
}

View file

@ -0,0 +1,87 @@
@php
$appName = config('core.app.name', 'Core PHP');
$appUrl = config('app.url', 'https://core.test');
$privacyUrl = config('core.urls.privacy', '/privacy');
$termsUrl = config('core.urls.terms', '/terms');
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'MCP Portal' }} - {{ $appName }}</title>
<meta name="description" content="{{ $description ?? 'Connect AI agents via Model Context Protocol' }}">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance
</head>
<body class="min-h-screen bg-white dark:bg-zinc-900">
<!-- Header -->
<header class="border-b border-zinc-200 dark:border-zinc-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center space-x-8">
<a href="{{ route('mcp.landing') }}" class="flex items-center space-x-2">
<span class="text-xl font-bold text-zinc-900 dark:text-white">MCP Portal</span>
</a>
<nav class="hidden md:flex items-center space-x-6 text-sm">
<a href="{{ route('mcp.servers.index') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Servers
</a>
<a href="{{ route('mcp.connect') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Setup Guide
</a>
</nav>
</div>
<div class="flex items-center space-x-4">
@php
$workspace = request()->attributes->get('mcp_workspace');
@endphp
@if($workspace)
<a href="{{ route('mcp.dashboard') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Dashboard
</a>
<a href="{{ route('mcp.keys') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
API Keys
</a>
<span class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $workspace->name }}
</span>
@else
<a href="{{ route('login') }}" class="text-sm text-cyan-600 hover:text-cyan-700 dark:text-cyan-400 dark:hover:text-cyan-300">
Sign in
</a>
@endif
<a href="{{ $appUrl }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
&larr; {{ $appName }}
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{{ $slot }}
</main>
<!-- Footer -->
<footer class="border-t border-zinc-200 dark:border-zinc-800 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<p class="text-sm text-zinc-500 dark:text-zinc-400">
&copy; {{ date('Y') }} {{ $appName }}. All rights reserved.
</p>
<div class="flex items-center space-x-6 text-sm text-zinc-500 dark:text-zinc-400">
<a href="{{ $privacyUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Privacy</a>
<a href="{{ $termsUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Terms</a>
</div>
</div>
</div>
</footer>
@fluxScripts
</body>
</html>