feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support

BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
This commit is contained in:
2025-10-27 09:31:28 +01:00
parent 799f74f00a
commit c8b47e647d
81 changed files with 6988 additions and 601 deletions

View File

@@ -0,0 +1,455 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mcp\Tools;
use App\Framework\HttpClient\HttpClient;
use App\Framework\HttpClient\HttpMethod;
use App\Framework\Mcp\McpTool;
/**
* Gitea Repository Management MCP Tools
*
* Provides AI-accessible Gitea API operations for repository management,
* SSH key setup, and deployment automation.
*/
final readonly class GiteaTools
{
public function __construct(
private HttpClient $httpClient,
private string $giteaUrl,
private string $giteaUsername,
private string $giteaPassword
) {
}
#[McpTool(
name: 'gitea_create_repository',
description: 'Create a new repository in Gitea'
)]
public function createRepository(
string $name,
string $description = '',
bool $private = true,
bool $autoInit = false,
string $defaultBranch = 'main'
): array {
$url = "{$this->giteaUrl}/api/v1/user/repos";
$data = [
'name' => $name,
'description' => $description,
'private' => $private,
'auto_init' => $autoInit,
'default_branch' => $defaultBranch,
];
$result = $this->makeRequest(HttpMethod::POST, $url, $data);
if ($result['success']) {
return [
'success' => true,
'repository' => [
'name' => $result['response']['name'] ?? $name,
'full_name' => $result['response']['full_name'] ?? "{$this->giteaUsername}/$name",
'clone_url' => $result['response']['clone_url'] ?? null,
'ssh_url' => $result['response']['ssh_url'] ?? null,
'html_url' => $result['response']['html_url'] ?? null,
'private' => $result['response']['private'] ?? $private,
'id' => $result['response']['id'] ?? null,
],
];
}
return $result;
}
#[McpTool(
name: 'gitea_list_repositories',
description: 'List all repositories for the authenticated user'
)]
public function listRepositories(): array
{
$url = "{$this->giteaUrl}/api/v1/user/repos";
$result = $this->makeRequest(HttpMethod::GET, $url);
if ($result['success']) {
$repos = array_map(function ($repo) {
return [
'name' => $repo['name'] ?? 'unknown',
'full_name' => $repo['full_name'] ?? 'unknown',
'description' => $repo['description'] ?? '',
'private' => $repo['private'] ?? false,
'clone_url' => $repo['clone_url'] ?? null,
'ssh_url' => $repo['ssh_url'] ?? null,
'html_url' => $repo['html_url'] ?? null,
];
}, $result['response'] ?? []);
return [
'success' => true,
'repositories' => $repos,
'count' => count($repos),
];
}
return $result;
}
#[McpTool(
name: 'gitea_get_repository',
description: 'Get details of a specific repository'
)]
public function getRepository(string $owner, string $repo): array
{
$url = "{$this->giteaUrl}/api/v1/repos/$owner/$repo";
$result = $this->makeRequest(HttpMethod::GET, $url);
if ($result['success']) {
$repo = $result['response'];
return [
'success' => true,
'repository' => [
'name' => $repo['name'] ?? 'unknown',
'full_name' => $repo['full_name'] ?? 'unknown',
'description' => $repo['description'] ?? '',
'private' => $repo['private'] ?? false,
'clone_url' => $repo['clone_url'] ?? null,
'ssh_url' => $repo['ssh_url'] ?? null,
'html_url' => $repo['html_url'] ?? null,
'default_branch' => $repo['default_branch'] ?? 'main',
'created_at' => $repo['created_at'] ?? null,
'updated_at' => $repo['updated_at'] ?? null,
],
];
}
return $result;
}
#[McpTool(
name: 'gitea_delete_repository',
description: 'Delete a repository'
)]
public function deleteRepository(string $owner, string $repo): array
{
$url = "{$this->giteaUrl}/api/v1/repos/$owner/$repo";
return $this->makeRequest(HttpMethod::DELETE, $url);
}
#[McpTool(
name: 'gitea_add_deploy_key',
description: 'Add an SSH deploy key to a repository'
)]
public function addDeployKey(
string $owner,
string $repo,
string $title,
string $key,
bool $readOnly = true
): array {
$url = "{$this->giteaUrl}/api/v1/repos/$owner/$repo/keys";
$data = [
'title' => $title,
'key' => $key,
'read_only' => $readOnly,
];
$result = $this->makeRequest(HttpMethod::POST, $url, $data);
if ($result['success']) {
return [
'success' => true,
'deploy_key' => [
'id' => $result['response']['id'] ?? null,
'title' => $result['response']['title'] ?? $title,
'key' => $result['response']['key'] ?? $key,
'read_only' => $result['response']['read_only'] ?? $readOnly,
'created_at' => $result['response']['created_at'] ?? null,
],
];
}
return $result;
}
#[McpTool(
name: 'gitea_list_deploy_keys',
description: 'List all deploy keys for a repository'
)]
public function listDeployKeys(string $owner, string $repo): array
{
$url = "{$this->giteaUrl}/api/v1/repos/$owner/$repo/keys";
$result = $this->makeRequest(HttpMethod::GET, $url);
if ($result['success']) {
$keys = array_map(function ($key) {
return [
'id' => $key['id'] ?? null,
'title' => $key['title'] ?? 'unknown',
'key' => $key['key'] ?? '',
'read_only' => $key['read_only'] ?? true,
'created_at' => $key['created_at'] ?? null,
];
}, $result['response'] ?? []);
return [
'success' => true,
'deploy_keys' => $keys,
'count' => count($keys),
];
}
return $result;
}
#[McpTool(
name: 'gitea_delete_deploy_key',
description: 'Delete a deploy key from a repository'
)]
public function deleteDeployKey(string $owner, string $repo, int $keyId): array
{
$url = "{$this->giteaUrl}/api/v1/repos/$owner/$repo/keys/$keyId";
return $this->makeRequest(HttpMethod::DELETE, $url);
}
#[McpTool(
name: 'gitea_add_user_ssh_key',
description: 'Add an SSH key to the authenticated user'
)]
public function addUserSshKey(string $title, string $key, bool $readOnly = false): array
{
$url = "{$this->giteaUrl}/api/v1/user/keys";
$data = [
'title' => $title,
'key' => $key,
'read_only' => $readOnly,
];
$result = $this->makeRequest(HttpMethod::POST, $url, $data);
if ($result['success']) {
return [
'success' => true,
'ssh_key' => [
'id' => $result['response']['id'] ?? null,
'title' => $result['response']['title'] ?? $title,
'key' => $result['response']['key'] ?? $key,
'read_only' => $result['response']['read_only'] ?? $readOnly,
'created_at' => $result['response']['created_at'] ?? null,
],
];
}
return $result;
}
#[McpTool(
name: 'gitea_list_user_ssh_keys',
description: 'List all SSH keys for the authenticated user'
)]
public function listUserSshKeys(): array
{
$url = "{$this->giteaUrl}/api/v1/user/keys";
$result = $this->makeRequest(HttpMethod::GET, $url);
if ($result['success']) {
$keys = array_map(function ($key) {
return [
'id' => $key['id'] ?? null,
'title' => $key['title'] ?? 'unknown',
'key' => $key['key'] ?? '',
'fingerprint' => $key['fingerprint'] ?? '',
'read_only' => $key['read_only'] ?? false,
'created_at' => $key['created_at'] ?? null,
];
}, $result['response'] ?? []);
return [
'success' => true,
'ssh_keys' => $keys,
'count' => count($keys),
];
}
return $result;
}
#[McpTool(
name: 'gitea_delete_user_ssh_key',
description: 'Delete an SSH key from the authenticated user'
)]
public function deleteUserSshKey(int $keyId): array
{
$url = "{$this->giteaUrl}/api/v1/user/keys/$keyId";
return $this->makeRequest(HttpMethod::DELETE, $url);
}
#[McpTool(
name: 'gitea_add_remote',
description: 'Add Gitea repository as git remote'
)]
public function addRemote(
string $remoteName,
string $owner,
string $repo,
bool $useSsh = true
): array {
// Get repository info first
$repoInfo = $this->getRepository($owner, $repo);
if (! $repoInfo['success']) {
return $repoInfo;
}
$url = $useSsh
? $repoInfo['repository']['ssh_url']
: $repoInfo['repository']['clone_url'];
if (! $url) {
return [
'success' => false,
'error' => 'Repository URL not found',
];
}
// Add remote via git command
$output = [];
$exitCode = 0;
$command = sprintf(
'git remote add %s %s 2>&1',
escapeshellarg($remoteName),
escapeshellarg($url)
);
exec($command, $output, $exitCode);
if ($exitCode !== 0) {
// Check if remote already exists
if (str_contains(implode("\n", $output), 'already exists')) {
return [
'success' => false,
'error' => 'Remote already exists',
'suggestion' => "Use 'git remote set-url $remoteName $url' to update",
];
}
return [
'success' => false,
'error' => 'Failed to add remote',
'output' => implode("\n", $output),
'exit_code' => $exitCode,
];
}
return [
'success' => true,
'remote_name' => $remoteName,
'url' => $url,
'use_ssh' => $useSsh,
];
}
#[McpTool(
name: 'gitea_webhook_create',
description: 'Create a webhook for a repository'
)]
public function createWebhook(
string $owner,
string $repo,
string $url,
string $contentType = 'json',
array $events = ['push'],
bool $active = true,
?string $secret = null
): array {
$hookUrl = "{$this->giteaUrl}/api/v1/repos/$owner/$repo/hooks";
$data = [
'type' => 'gitea',
'config' => [
'url' => $url,
'content_type' => $contentType,
'secret' => $secret ?? '',
],
'events' => $events,
'active' => $active,
];
$result = $this->makeRequest(HttpMethod::POST, $hookUrl, $data);
if ($result['success']) {
return [
'success' => true,
'webhook' => [
'id' => $result['response']['id'] ?? null,
'url' => $result['response']['config']['url'] ?? $url,
'events' => $result['response']['events'] ?? $events,
'active' => $result['response']['active'] ?? $active,
'created_at' => $result['response']['created_at'] ?? null,
],
];
}
return $result;
}
// ==================== Private Helper Methods ====================
private function makeRequest(HttpMethod $method, string $url, ?array $data = null): array
{
try {
$options = [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => 'Basic ' . base64_encode("{$this->giteaUsername}:{$this->giteaPassword}"),
],
'verify_ssl' => false, // For self-signed certificates
];
if ($data !== null) {
$options['json'] = $data;
}
$response = $this->httpClient->request($method, $url, $options);
$statusCode = $response->getStatusCode();
$body = $response->getBody();
// Decode JSON response
$decoded = json_decode($body, true);
if ($statusCode >= 200 && $statusCode < 300) {
return [
'success' => true,
'response' => $decoded,
'http_code' => $statusCode,
];
}
return [
'success' => false,
'error' => $decoded['message'] ?? 'HTTP error ' . $statusCode,
'response' => $decoded,
'http_code' => $statusCode,
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => 'Request failed: ' . $e->getMessage(),
'exception' => get_class($e),
];
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Framework\Mcp\Tools;
use App\Framework\Config\Environment;
use App\Framework\DI\Initializer;
use App\Framework\HttpClient\HttpClient;
/**
* Initializer for Gitea MCP Tools
*/
final readonly class GiteaToolsInitializer
{
public function __construct(
private HttpClient $httpClient,
private Environment $environment
) {
}
#[Initializer]
public function __invoke(): GiteaTools
{
// Get Gitea configuration from environment
$giteaUrl = $this->environment->get('GITEA_URL', 'https://localhost:9443');
$giteaUsername = $this->environment->get('GITEA_USERNAME', 'michael');
$giteaPassword = $this->environment->get('GITEA_PASSWORD', 'GiteaAdmin2024');
return new GiteaTools(
$this->httpClient,
$giteaUrl,
$giteaUsername,
$giteaPassword
);
}
}