diff --git a/src/Core/Events/McpToolsRegistering.php b/src/Core/Events/McpToolsRegistering.php index 057ea9d..7a4a54f 100644 --- a/src/Core/Events/McpToolsRegistering.php +++ b/src/Core/Events/McpToolsRegistering.php @@ -11,8 +11,6 @@ declare(strict_types=1); namespace Core\Events; -use Core\Front\Mcp\Contracts\McpToolHandler; - /** * Fired when MCP (Model Context Protocol) tools are being registered. * @@ -27,8 +25,9 @@ use Core\Front\Mcp\Contracts\McpToolHandler; * * ## Handler Requirements * - * Each handler class must implement `McpToolHandler` interface. Handlers - * define the tools, their input schemas, and execution logic. + * Each handler class must implement `McpToolHandler` interface (from php-mcp). + * Handlers define the tools, their input schemas, and execution logic. + * Validation is performed by the MCP package at runtime. * * ## Usage Example * @@ -43,9 +42,6 @@ use Core\Front\Mcp\Contracts\McpToolHandler; * $event->handler(InventoryQueryHandler::class); * } * ``` - * - * - * @see \Core\Front\Mcp\Contracts\McpToolHandler */ class McpToolsRegistering extends LifecycleEvent { @@ -56,14 +52,9 @@ class McpToolsRegistering extends LifecycleEvent * Register an MCP tool handler class. * * @param string $handlerClass Fully qualified class name implementing McpToolHandler - * - * @throws \InvalidArgumentException If class doesn't implement McpToolHandler */ public function handler(string $handlerClass): void { - if (! is_a($handlerClass, McpToolHandler::class, true)) { - throw new \InvalidArgumentException("{$handlerClass} must implement McpToolHandler"); - } $this->handlers[] = $handlerClass; } diff --git a/src/Core/Front/Api/ApiVersionService.php b/src/Core/Front/Api/ApiVersionService.php deleted file mode 100644 index 5b889d2..0000000 --- a/src/Core/Front/Api/ApiVersionService.php +++ /dev/null @@ -1,253 +0,0 @@ -versions->isV2($request)) { - * return $this->indexV2($request); - * } - * return $this->indexV1($request); - * } - * } - * ``` - * - * ## Version Negotiation - * - * The service supports version negotiation where controllers can provide - * different responses based on the requested version: - * - * ```php - * return $this->versions->negotiate($request, [ - * 1 => fn() => $this->responseV1(), - * 2 => fn() => $this->responseV2(), - * ]); - * ``` - */ -class ApiVersionService -{ - /** - * Get the current API version from the request. - * - * Returns null if no version middleware has processed the request. - */ - public function current(?Request $request = null): ?int - { - $request ??= request(); - - return $request->attributes->get('api_version'); - } - - /** - * Get the current API version as a string (e.g., 'v1'). - */ - public function currentString(?Request $request = null): ?string - { - $request ??= request(); - - return $request->attributes->get('api_version_string'); - } - - /** - * Check if the request is for a specific version. - */ - public function is(int $version, ?Request $request = null): bool - { - return $this->current($request) === $version; - } - - /** - * Check if the request is for version 1. - */ - public function isV1(?Request $request = null): bool - { - return $this->is(1, $request); - } - - /** - * Check if the request is for version 2. - */ - public function isV2(?Request $request = null): bool - { - return $this->is(2, $request); - } - - /** - * Check if the request version is at least the given version. - */ - public function isAtLeast(int $version, ?Request $request = null): bool - { - $current = $this->current($request); - - return $current !== null && $current >= $version; - } - - /** - * Check if the current version is deprecated. - */ - public function isDeprecated(?Request $request = null): bool - { - $current = $this->current($request); - $deprecated = config('api.versioning.deprecated', []); - - return $current !== null && in_array($current, $deprecated, true); - } - - /** - * Get the configured default version. - */ - public function defaultVersion(): int - { - return (int) config('api.versioning.default', 1); - } - - /** - * Get the current/latest version. - */ - public function latestVersion(): int - { - return (int) config('api.versioning.current', 1); - } - - /** - * Get all supported versions. - * - * @return array - */ - public function supportedVersions(): array - { - return config('api.versioning.supported', [1]); - } - - /** - * Get all deprecated versions. - * - * @return array - */ - public function deprecatedVersions(): array - { - return config('api.versioning.deprecated', []); - } - - /** - * Get sunset dates for versions. - * - * @return array - */ - public function sunsetDates(): array - { - return config('api.versioning.sunset', []); - } - - /** - * Check if a version is supported. - */ - public function isSupported(int $version): bool - { - return in_array($version, $this->supportedVersions(), true); - } - - /** - * Negotiate response based on API version. - * - * Calls the appropriate handler based on the request's API version. - * Falls back to lower version handlers if exact match not found. - * - * ```php - * return $versions->negotiate($request, [ - * 1 => fn() => ['format' => 'v1'], - * 2 => fn() => ['format' => 'v2', 'extra' => 'field'], - * ]); - * ``` - * - * @param array $handlers Version handlers keyed by version number - * @return mixed Result from the appropriate handler - * - * @throws \InvalidArgumentException If no suitable handler found - */ - public function negotiate(Request $request, array $handlers): mixed - { - $version = $this->current($request) ?? $this->defaultVersion(); - - // Try exact match first - if (isset($handlers[$version])) { - return $handlers[$version](); - } - - // Fall back to highest version that's <= requested version - krsort($handlers); - foreach ($handlers as $handlerVersion => $handler) { - if ($handlerVersion <= $version) { - return $handler(); - } - } - - // No suitable handler found - throw new \InvalidArgumentException( - "No handler found for API version {$version}. Available versions: ".implode(', ', array_keys($handlers)) - ); - } - - /** - * Transform response data based on API version. - * - * Useful for removing or adding fields based on version. - * - * ```php - * return $versions->transform($request, $data, [ - * 1 => fn($data) => Arr::except($data, ['new_field']), - * 2 => fn($data) => $data, - * ]); - * ``` - * - * @param array $transformers Version transformers - */ - public function transform(Request $request, mixed $data, array $transformers): mixed - { - $version = $this->current($request) ?? $this->defaultVersion(); - - // Try exact match first - if (isset($transformers[$version])) { - return $transformers[$version]($data); - } - - // Fall back to highest version that's <= requested version - krsort($transformers); - foreach ($transformers as $transformerVersion => $transformer) { - if ($transformerVersion <= $version) { - return $transformer($data); - } - } - - // No transformer, return data unchanged - return $data; - } -} diff --git a/src/Core/Front/Api/Boot.php b/src/Core/Front/Api/Boot.php deleted file mode 100644 index 545bfe6..0000000 --- a/src/Core/Front/Api/Boot.php +++ /dev/null @@ -1,111 +0,0 @@ - [ - * 'default' => 1, // Default version when none specified - * 'current' => 1, // Current/latest version - * 'supported' => [1], // List of supported versions - * 'deprecated' => [], // Deprecated but still supported versions - * 'sunset' => [], // Sunset dates: [1 => '2025-06-01'] - * ], - * ``` - * - * @see ApiVersion Middleware for version parsing - * @see ApiVersionService Service for programmatic version checks - * @see VersionedRoutes Helper for version-based route registration - */ -class Boot extends ServiceProvider -{ - /** - * Configure api middleware group. - */ - public static function middleware(Middleware $middleware): void - { - $middleware->group('api', [ - \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ]); - - // Register versioning middleware aliases - $middleware->alias([ - 'api.version' => ApiVersion::class, - 'api.sunset' => ApiSunset::class, - ]); - } - - public function register(): void - { - // Merge API configuration - $this->mergeConfigFrom(__DIR__.'/config.php', 'api'); - - // Register API version service as singleton - $this->app->singleton(ApiVersionService::class); - } - - public function boot(): void - { - $this->configureRateLimiting(); - $this->registerMiddlewareAliases(); - - // Fire ApiRoutesRegistering event for lazy-loaded modules - LifecycleEventProvider::fireApiRoutes(); - } - - /** - * Register middleware aliases via router. - * - * This ensures aliases are available even if the static middleware() - * method isn't called (e.g., in testing or custom bootstrap). - */ - protected function registerMiddlewareAliases(): void - { - /** @var Router $router */ - $router = $this->app->make(Router::class); - - $router->aliasMiddleware('api.version', ApiVersion::class); - $router->aliasMiddleware('api.sunset', ApiSunset::class); - } - - /** - * Configure API rate limiting. - */ - protected function configureRateLimiting(): void - { - RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); - }); - } -} diff --git a/src/Core/Front/Api/Middleware/ApiSunset.php b/src/Core/Front/Api/Middleware/ApiSunset.php deleted file mode 100644 index c853f9a..0000000 --- a/src/Core/Front/Api/Middleware/ApiSunset.php +++ /dev/null @@ -1,112 +0,0 @@ -group(function () { - * Route::get('/legacy-endpoint', LegacyController::class); - * }); - * ``` - * - * Or with a replacement link: - * - * ```php - * Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () { - * Route::get('/old-endpoint', OldController::class); - * }); - * ``` - * - * ## Response Headers - * - * The middleware adds these headers: - * - Sunset: - * - Deprecation: true - * - Link: ; rel="successor-version" (if replacement provided) - * - * @see https://datatracker.ietf.org/doc/html/rfc8594 RFC 8594: The "Sunset" HTTP Header Field - */ -class ApiSunset -{ - /** - * Handle an incoming request. - * - * @param string $sunsetDate The sunset date (YYYY-MM-DD or RFC7231 format) - * @param string|null $replacement Optional replacement endpoint URL - */ - public function handle(Request $request, Closure $next, string $sunsetDate, ?string $replacement = null): Response - { - /** @var Response $response */ - $response = $next($request); - - // Convert date to RFC7231 format if needed - $formattedDate = $this->formatSunsetDate($sunsetDate); - - // Add Sunset header - $response->headers->set('Sunset', $formattedDate); - - // Add Deprecation header - $response->headers->set('Deprecation', 'true'); - - // Add warning header - $version = $request->attributes->get('api_version', 'unknown'); - $response->headers->set( - 'X-API-Warn', - "This endpoint is deprecated and will be removed on {$sunsetDate}." - ); - - // Add Link header for replacement if provided - if ($replacement !== null) { - $response->headers->set('Link', "<{$replacement}>; rel=\"successor-version\""); - } - - return $response; - } - - /** - * Format the sunset date to RFC7231 format. - * - * Accepts dates in YYYY-MM-DD format or already-formatted RFC7231 dates. - */ - protected function formatSunsetDate(string $date): string - { - // Check if already in RFC7231 format (contains comma, day name) - if (str_contains($date, ',')) { - return $date; - } - - try { - return (new \DateTimeImmutable($date)) - ->setTimezone(new \DateTimeZone('GMT')) - ->format(\DateTimeInterface::RFC7231); - } catch (\Exception) { - // If parsing fails, return as-is - return $date; - } - } -} diff --git a/src/Core/Front/Api/Middleware/ApiVersion.php b/src/Core/Front/Api/Middleware/ApiVersion.php deleted file mode 100644 index 52c659b..0000000 --- a/src/Core/Front/Api/Middleware/ApiVersion.php +++ /dev/null @@ -1,246 +0,0 @@ -attributes->get('api_version') - returns integer (e.g., 1, 2) - * - $request->attributes->get('api_version_string') - returns string (e.g., 'v1', 'v2') - * - * ## Configuration - * - * Configure in config/api.php: - * ```php - * 'versioning' => [ - * 'default' => 1, // Default version when none specified - * 'current' => 1, // Current/latest version - * 'supported' => [1], // List of supported versions - * 'deprecated' => [], // List of deprecated (but still supported) versions - * 'sunset' => [], // Versions with sunset dates: [1 => '2025-06-01'] - * ], - * ``` - * - * ## Usage in Routes - * - * ```php - * // Apply to specific routes - * Route::middleware('api.version')->group(function () { - * Route::get('/users', [UserController::class, 'index']); - * }); - * - * // Or with version constraint - * Route::middleware('api.version:2')->group(function () { - * // Only accepts v2 requests - * }); - * ``` - * - * ## Deprecation Headers - * - * When a request uses a deprecated API version, the response includes: - * - Deprecation: true - * - Sunset: (if configured) - * - X-API-Warn: "API version X is deprecated..." - * - * @see ApiVersionService For programmatic version checks - */ -class ApiVersion -{ - /** - * Handle an incoming request. - * - * @param int|null $requiredVersion Minimum version required (optional) - */ - public function handle(Request $request, Closure $next, ?int $requiredVersion = null): Response - { - $version = $this->resolveVersion($request); - $versionConfig = config('api.versioning', []); - - $default = $versionConfig['default'] ?? 1; - $current = $versionConfig['current'] ?? 1; - $supported = $versionConfig['supported'] ?? [1]; - $deprecated = $versionConfig['deprecated'] ?? []; - $sunset = $versionConfig['sunset'] ?? []; - - // Use default if no version specified - if ($version === null) { - $version = $default; - } - - // Validate version is supported - if (! in_array($version, $supported, true)) { - return $this->unsupportedVersion($version, $supported, $current); - } - - // Check minimum version requirement - if ($requiredVersion !== null && $version < $requiredVersion) { - return $this->versionTooLow($version, $requiredVersion); - } - - // Store version in request - $request->attributes->set('api_version', $version); - $request->attributes->set('api_version_string', "v{$version}"); - - /** @var Response $response */ - $response = $next($request); - - // Add version header to response - $response->headers->set('X-API-Version', (string) $version); - - // Add deprecation headers if applicable - if (in_array($version, $deprecated, true)) { - $response->headers->set('Deprecation', 'true'); - $response->headers->set('X-API-Warn', "API version {$version} is deprecated. Please upgrade to v{$current}."); - - // Add Sunset header if configured - if (isset($sunset[$version])) { - $sunsetDate = $sunset[$version]; - // Convert to HTTP date format if not already - if (! str_contains($sunsetDate, ',')) { - $sunsetDate = (new \DateTimeImmutable($sunsetDate))->format(\DateTimeInterface::RFC7231); - } - $response->headers->set('Sunset', $sunsetDate); - } - } - - return $response; - } - - /** - * Resolve the API version from the request. - * - * Priority order: - * 1. URL path (/api/v1/...) - * 2. Accept-Version header - * 3. Accept header vendor type - */ - protected function resolveVersion(Request $request): ?int - { - // 1. Check URL path for version prefix - $version = $this->versionFromPath($request); - if ($version !== null) { - return $version; - } - - // 2. Check Accept-Version header - $version = $this->versionFromHeader($request); - if ($version !== null) { - return $version; - } - - // 3. Check Accept header for vendor type - return $this->versionFromAcceptHeader($request); - } - - /** - * Extract version from URL path. - * - * Matches: /api/v1/..., /api/v2/... - */ - protected function versionFromPath(Request $request): ?int - { - $path = $request->path(); - - // Match /api/v{n}/ or /v{n}/ at the start - if (preg_match('#^(?:api/)?v(\d+)(?:/|$)#', $path, $matches)) { - return (int) $matches[1]; - } - - return null; - } - - /** - * Extract version from Accept-Version header. - * - * Accepts: v1, v2, 1, 2 - */ - protected function versionFromHeader(Request $request): ?int - { - $header = $request->header('Accept-Version'); - - if ($header === null) { - return null; - } - - // Strip 'v' prefix if present - $version = ltrim($header, 'vV'); - - if (is_numeric($version)) { - return (int) $version; - } - - return null; - } - - /** - * Extract version from Accept header vendor type. - * - * Matches: application/vnd.hosthub.v1+json - */ - protected function versionFromAcceptHeader(Request $request): ?int - { - $accept = $request->header('Accept', ''); - - // Match vendor media type: application/vnd.{name}.v{n}+json - if (preg_match('#application/vnd\.[^.]+\.v(\d+)\+#', $accept, $matches)) { - return (int) $matches[1]; - } - - return null; - } - - /** - * Return 400 response for unsupported version. - * - * @param array $supported - */ - protected function unsupportedVersion(int $requested, array $supported, int $current): Response - { - return response()->json([ - 'error' => 'unsupported_api_version', - 'message' => "API version {$requested} is not supported.", - 'requested_version' => $requested, - 'supported_versions' => $supported, - 'current_version' => $current, - 'hint' => 'Use Accept-Version header or URL prefix (e.g., /api/v1/) to specify version.', - ], 400, [ - 'X-API-Version' => (string) $current, - ]); - } - - /** - * Return 400 response when version is too low. - */ - protected function versionTooLow(int $requested, int $required): Response - { - return response()->json([ - 'error' => 'api_version_too_low', - 'message' => "This endpoint requires API version {$required} or higher.", - 'requested_version' => $requested, - 'minimum_version' => $required, - ], 400, [ - 'X-API-Version' => (string) $requested, - ]); - } -} diff --git a/src/Core/Front/Api/README.md b/src/Core/Front/Api/README.md deleted file mode 100644 index 45689c6..0000000 --- a/src/Core/Front/Api/README.md +++ /dev/null @@ -1,266 +0,0 @@ -# API Versioning - -Core PHP Framework provides built-in API versioning support with deprecation handling and sunset headers. - -## Quick Start - -### 1. Configure Versions - -Add to your `config/api.php`: - -```php -'versioning' => [ - 'default' => 1, // Version when none specified - 'current' => 2, // Latest/current version - 'supported' => [1, 2], // All supported versions - 'deprecated' => [1], // Deprecated but still working - 'sunset' => [ // Removal dates - 1 => '2025-12-31', - ], -], -``` - -### 2. Apply Middleware - -The `api.version` middleware is automatically available. Apply it to routes: - -```php -// Version-agnostic routes (uses default version) -Route::middleware('api.version')->group(function () { - Route::get('/status', StatusController::class); -}); - -// Version-specific routes with URL prefix -use Core\Front\Api\VersionedRoutes; - -VersionedRoutes::v1(function () { - Route::get('/users', [UserController::class, 'indexV1']); -}); - -VersionedRoutes::v2(function () { - Route::get('/users', [UserController::class, 'indexV2']); -}); -``` - -### 3. Version Negotiation in Controllers - -```php -use Core\Front\Api\ApiVersionService; - -class UserController -{ - public function __construct( - protected ApiVersionService $versions - ) {} - - public function index(Request $request) - { - return $this->versions->negotiate($request, [ - 1 => fn() => $this->indexV1(), - 2 => fn() => $this->indexV2(), - ]); - } -} -``` - -## Version Resolution - -The middleware resolves the API version from (in priority order): - -1. **URL Path**: `/api/v1/users` or `/v2/users` -2. **Accept-Version Header**: `Accept-Version: v1` or `Accept-Version: 2` -3. **Accept Header**: `Accept: application/vnd.hosthub.v1+json` -4. **Default**: Falls back to configured default version - -## Response Headers - -Successful responses include: - -``` -X-API-Version: 2 -``` - -Deprecated versions also include: - -``` -Deprecation: true -X-API-Warn: API version 1 is deprecated. Please upgrade to v2. -Sunset: Wed, 31 Dec 2025 00:00:00 GMT -``` - -## Error Responses - -### Unsupported Version (400) - -```json -{ - "error": "unsupported_api_version", - "message": "API version 99 is not supported.", - "requested_version": 99, - "supported_versions": [1, 2], - "current_version": 2, - "hint": "Use Accept-Version header or URL prefix (e.g., /api/v1/) to specify version." -} -``` - -### Version Too Low (400) - -```json -{ - "error": "api_version_too_low", - "message": "This endpoint requires API version 2 or higher.", - "requested_version": 1, - "minimum_version": 2 -} -``` - -## Versioned Routes Helper - -The `VersionedRoutes` class provides a fluent API for registering version-specific routes: - -```php -use Core\Front\Api\VersionedRoutes; - -// Simple version registration -VersionedRoutes::v1(function () { - Route::get('/users', UserController::class); -}); - -// With URL prefix (default) -VersionedRoutes::v2(function () { - Route::get('/users', UserControllerV2::class); -}); // Accessible at /api/v2/users - -// Header-only versioning (no URL prefix) -VersionedRoutes::version(2) - ->withoutPrefix() - ->routes(function () { - Route::get('/users', UserControllerV2::class); - }); // Accessible at /api/users with Accept-Version: 2 - -// Multiple versions for the same routes -VersionedRoutes::versions([1, 2], function () { - Route::get('/health', HealthController::class); -}); - -// Deprecated version with sunset -VersionedRoutes::v1() - ->deprecated('2025-06-01') - ->routes(function () { - Route::get('/legacy', LegacyController::class); - }); -``` - -## ApiVersionService - -Inject `ApiVersionService` for programmatic version checks: - -```php -use Core\Front\Api\ApiVersionService; - -class UserController -{ - public function __construct( - protected ApiVersionService $versions - ) {} - - public function show(Request $request, User $user) - { - $data = $user->toArray(); - - // Version-specific transformations - return $this->versions->transform($request, $data, [ - 1 => fn($d) => Arr::except($d, ['created_at', 'metadata']), - 2 => fn($d) => $d, - ]); - } -} -``` - -### Available Methods - -| Method | Description | -|--------|-------------| -| `current($request)` | Get version number (e.g., 1, 2) | -| `currentString($request)` | Get version string (e.g., 'v1') | -| `is($version, $request)` | Check exact version | -| `isV1($request)` | Check if version 1 | -| `isV2($request)` | Check if version 2 | -| `isAtLeast($version, $request)` | Check minimum version | -| `isDeprecated($request)` | Check if version is deprecated | -| `defaultVersion()` | Get configured default | -| `latestVersion()` | Get current/latest version | -| `supportedVersions()` | Get all supported versions | -| `deprecatedVersions()` | Get deprecated versions | -| `sunsetDates()` | Get sunset dates map | -| `isSupported($version)` | Check if version is supported | -| `negotiate($request, $handlers)` | Call version-specific handler | -| `transform($request, $data, $transformers)` | Transform data per version | - -## Sunset Middleware - -For endpoint-specific deprecation, use the `api.sunset` middleware: - -```php -Route::middleware('api.sunset:2025-06-01')->group(function () { - Route::get('/legacy-endpoint', LegacyController::class); -}); - -// With replacement hint -Route::middleware('api.sunset:2025-06-01,/api/v2/new-endpoint')->group(function () { - Route::get('/old-endpoint', OldController::class); -}); -``` - -Adds headers: - -``` -Sunset: Sun, 01 Jun 2025 00:00:00 GMT -Deprecation: true -X-API-Warn: This endpoint is deprecated and will be removed on 2025-06-01. -Link: ; rel="successor-version" -``` - -## Versioning Strategy - -### Guidelines - -1. **Add, don't remove**: New fields can be added to any version -2. **New version for breaking changes**: Removing/renaming fields requires new version -3. **Deprecate before removal**: Give clients time to migrate -4. **Document changes**: Maintain changelog per version - -### Version Lifecycle - -``` -v1: Active -> Deprecated (with sunset) -> Removed from supported -v2: Active (current) -v3: Future -``` - -### Environment Variables - -```env -API_VERSION_DEFAULT=1 -API_VERSION_CURRENT=2 -API_VERSIONS_SUPPORTED=1,2 -API_VERSIONS_DEPRECATED=1 -``` - -## Testing - -Test versioned endpoints by setting the Accept-Version header: - -```php -$response = $this->withHeaders([ - 'Accept-Version' => 'v2', -])->getJson('/api/users'); - -$response->assertHeader('X-API-Version', '2'); -``` - -Or use URL prefix: - -```php -$response = $this->getJson('/api/v2/users'); -``` diff --git a/src/Core/Front/Api/VersionedRoutes.php b/src/Core/Front/Api/VersionedRoutes.php deleted file mode 100644 index 5ebe22f..0000000 --- a/src/Core/Front/Api/VersionedRoutes.php +++ /dev/null @@ -1,248 +0,0 @@ -withoutPrefix() - * ->routes(function () { - * Route::get('/users', ...); // Accessible at /api/users with Accept-Version: 1 - * }); - * ``` - * - * ## Multiple Versions - * - * Register the same routes for multiple versions: - * - * ```php - * VersionedRoutes::versions([1, 2], function () { - * Route::get('/status', [StatusController::class, 'index']); - * }); - * ``` - * - * ## Deprecation - * - * Mark a version as deprecated with custom sunset date: - * - * ```php - * VersionedRoutes::v1() - * ->deprecated('2025-06-01') - * ->routes(function () { - * Route::get('/legacy', ...); - * }); - * ``` - */ -class VersionedRoutes -{ - protected int $version; - - protected bool $usePrefix = true; - - protected ?string $sunsetDate = null; - - protected bool $isDeprecated = false; - - /** - * @var array - */ - protected array $middleware = []; - - /** - * Create a new versioned routes instance. - */ - public function __construct(int $version) - { - $this->version = $version; - } - - /** - * Create routes for version 1. - */ - public static function v1(?callable $routes = null): static - { - $instance = new static(1); - - if ($routes !== null) { - $instance->routes($routes); - } - - return $instance; - } - - /** - * Create routes for version 2. - */ - public static function v2(?callable $routes = null): static - { - $instance = new static(2); - - if ($routes !== null) { - $instance->routes($routes); - } - - return $instance; - } - - /** - * Create routes for a specific version. - */ - public static function version(int $version): static - { - return new static($version); - } - - /** - * Register routes for multiple versions. - * - * @param array $versions - */ - public static function versions(array $versions, callable $routes): void - { - foreach ($versions as $version) { - (new static($version))->routes($routes); - } - } - - /** - * Don't use URL prefix for this version. - * - * Routes will be accessible without /v{n} prefix but will - * still require version header for version-specific behaviour. - */ - public function withoutPrefix(): static - { - $this->usePrefix = false; - - return $this; - } - - /** - * Use URL prefix for this version. - * - * This is the default behaviour. - */ - public function withPrefix(): static - { - $this->usePrefix = true; - - return $this; - } - - /** - * Mark this version as deprecated. - * - * @param string|null $sunsetDate Optional sunset date (YYYY-MM-DD or RFC7231 format) - */ - public function deprecated(?string $sunsetDate = null): static - { - $this->isDeprecated = true; - $this->sunsetDate = $sunsetDate; - - return $this; - } - - /** - * Add additional middleware to the version routes. - * - * @param array|string $middleware - */ - public function middleware(array|string $middleware): static - { - $this->middleware = array_merge( - $this->middleware, - is_array($middleware) ? $middleware : [$middleware] - ); - - return $this; - } - - /** - * Register the routes for this version. - */ - public function routes(callable $routes): void - { - $attributes = $this->buildRouteAttributes(); - - Route::group($attributes, $routes); - } - - /** - * Build the route group attributes. - * - * @return array - */ - protected function buildRouteAttributes(): array - { - $attributes = [ - 'middleware' => $this->buildMiddleware(), - ]; - - if ($this->usePrefix) { - $attributes['prefix'] = "v{$this->version}"; - } - - return $attributes; - } - - /** - * Build the middleware stack for this version. - * - * @return array - */ - protected function buildMiddleware(): array - { - $middleware = ["api.version:{$this->version}"]; - - if ($this->isDeprecated && $this->sunsetDate) { - $middleware[] = "api.sunset:{$this->sunsetDate}"; - } - - return array_merge($middleware, $this->middleware); - } -} diff --git a/src/Core/Front/Api/config.php b/src/Core/Front/Api/config.php deleted file mode 100644 index 852acc3..0000000 --- a/src/Core/Front/Api/config.php +++ /dev/null @@ -1,78 +0,0 @@ - [ - // Default version when no version specified in request - // Clients should always specify version explicitly - 'default' => (int) env('API_VERSION_DEFAULT', 1), - - // Current/latest API version - // Used in deprecation warnings to suggest upgrade path - 'current' => (int) env('API_VERSION_CURRENT', 1), - - // Supported API versions (all still functional) - // Remove versions from this list to disable them entirely - 'supported' => array_map('intval', array_filter( - explode(',', env('API_VERSIONS_SUPPORTED', '1')) - )), - - // Deprecated versions (still work but warn clients) - // Responses include Deprecation: true header - 'deprecated' => array_map('intval', array_filter( - explode(',', env('API_VERSIONS_DEPRECATED', '')) - )), - - // Sunset dates for deprecated versions - // Format: [version => 'YYYY-MM-DD'] - // After this date, version should be removed from 'supported' - 'sunset' => [ - // Example: 1 => '2025-12-31', - ], - ], - - /* - |-------------------------------------------------------------------------- - | Response Headers - |-------------------------------------------------------------------------- - | - | Standard headers added to API responses. - | - */ - 'headers' => [ - // Add X-API-Version header to all responses - 'include_version' => true, - - // Add deprecation warnings for old versions - 'include_deprecation' => true, - ], -]; diff --git a/src/Core/Front/Boot.php b/src/Core/Front/Boot.php index 2af5e78..e230dd4 100644 --- a/src/Core/Front/Boot.php +++ b/src/Core/Front/Boot.php @@ -17,15 +17,17 @@ use Illuminate\Support\AggregateServiceProvider; /** * Core front-end module - I/O translation layer. * - * Seven frontages, each translating a transport protocol: + * Six frontages bundled in the framework, each translating a transport protocol: * Web - HTTP → HTML (public marketing) * Client - HTTP → HTML (namespace owner dashboard) * Admin - HTTP → HTML (backend admin dashboard) - * Api - HTTP → JSON (REST API) - * Mcp - HTTP → JSON-RPC (MCP protocol) * Cli - Artisan commands (console context) * Stdio - stdin/stdout (CLI pipes, MCP stdio) * Components - View namespaces (shared across HTTP frontages) + * + * Additional frontages provided by their packages (auto-discovered): + * Api - HTTP → JSON (REST API) — php-api + * Mcp - HTTP → JSON-RPC (MCP protocol) — php-mcp */ class Boot extends AggregateServiceProvider { @@ -33,8 +35,6 @@ class Boot extends AggregateServiceProvider Web\Boot::class, Client\Boot::class, Admin\Boot::class, - Api\Boot::class, - Mcp\Boot::class, Cli\Boot::class, Stdio\Boot::class, Components\Boot::class, @@ -49,7 +49,17 @@ class Boot extends AggregateServiceProvider Web\Boot::middleware($middleware); Client\Boot::middleware($middleware); Admin\Boot::middleware($middleware); - Api\Boot::middleware($middleware); - Mcp\Boot::middleware($middleware); + + // API and MCP groups — inlined because middleware() runs during + // Application::configure(), before package providers load. + // Packages add their own aliases during boot via lifecycle events. + $middleware->group('api', [ + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); + $middleware->group('mcp', [ + \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); } } diff --git a/src/Core/Front/Components/View/Blade/layouts/mcp.blade.php b/src/Core/Front/Components/View/Blade/layouts/mcp.blade.php deleted file mode 100644 index 04f0e9a..0000000 --- a/src/Core/Front/Components/View/Blade/layouts/mcp.blade.php +++ /dev/null @@ -1,87 +0,0 @@ -@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 - - - - - - - - {{ $title ?? 'MCP Portal' }} - {{ $appName }} - - - @vite(['resources/css/app.css', 'resources/js/app.js']) - @fluxAppearance - - - -
-
-
- -
- @php - $workspace = request()->attributes->get('mcp_workspace'); - @endphp - @if($workspace) - - Dashboard - - - API Keys - - - {{ $workspace->name }} - - @else - - Sign in - - @endif - - ← {{ $appName }} - -
-
-
-
- - -
- {{ $slot }} -
- - -
-
-
-

- © {{ date('Y') }} {{ $appName }}. All rights reserved. -

-
- Privacy - Terms -
-
-
-
- - @fluxScripts - - diff --git a/src/Core/Front/Mcp/Boot.php b/src/Core/Front/Mcp/Boot.php deleted file mode 100644 index c0726b4..0000000 --- a/src/Core/Front/Mcp/Boot.php +++ /dev/null @@ -1,50 +0,0 @@ -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(); - } -} diff --git a/src/Core/Front/Mcp/Contracts/McpToolHandler.php b/src/Core/Front/Mcp/Contracts/McpToolHandler.php deleted file mode 100644 index e223f4b..0000000 --- a/src/Core/Front/Mcp/Contracts/McpToolHandler.php +++ /dev/null @@ -1,48 +0,0 @@ -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; - } -} diff --git a/src/Core/LifecycleEventProvider.php b/src/Core/LifecycleEventProvider.php index c230553..b514e08 100644 --- a/src/Core/LifecycleEventProvider.php +++ b/src/Core/LifecycleEventProvider.php @@ -73,7 +73,7 @@ use Livewire\Livewire; * │ Processes: views, translations, livewire, routes │ * │ ('admin' middleware) │ * │ │ - * ├─── Front/Api/Boot ────────────────────────────────────────────────── │ + * ├─── Front/Api/Boot (php-api package) ─────────────────────────────── │ * │ └── LifecycleEventProvider::fireApiRoutes() │ * │ Fires: ApiRoutesRegistering │ * │ Processes: routes ('api' middleware) │ @@ -88,7 +88,7 @@ use Livewire\Livewire; * │ Fires: ConsoleBooting │ * │ Processes: command classes │ * │ │ - * └─── Front/Mcp/Boot ────────────────────────────────────────────────── │ + * └─── Front/Mcp/Boot (php-mcp package) ─────────────────────────────── │ * ├── LifecycleEventProvider::fireMcpRoutes() │ * │ Fires: McpRoutesRegistering │ * │ Processes: routes ('mcp' middleware) │ @@ -456,7 +456,7 @@ class LifecycleEventProvider extends ServiceProvider * * @return array Fully qualified class names of McpToolHandler implementations * - * @see \Core\Front\Mcp\Contracts\McpToolHandler + * @see \Core\Front\Mcp\Contracts\McpToolHandler (in php-mcp package) */ public static function fireMcpTools(): array {