diff --git a/.husky/pre-commit b/.husky/pre-commit index f41d0d5c22..aa6c6d25c7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -mago format --check && composer validate && composer test && pnpm pretty-quick --staged && pnpm eslint . && pnpm run tsc && pnpm test +mago format --check && composer validate && composer test && pnpm pretty-quick --staged && pnpm run eslint && pnpm run tsc && pnpm test diff --git a/.husky/pre-push b/.husky/pre-push index 32998bfe67..7656dec3bf 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1 @@ -composer validate && composer test && pnpm pretty-quick --staged && pnpm eslint . && pnpm run tsc && pnpm test +composer validate && composer test && pnpm pretty-quick --staged && pnpm run eslint && pnpm run tsc && pnpm test diff --git a/CHANGELOG.md b/CHANGELOG.md index cbf243e032..690027b2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ This is a log of major user-visible changes in each phpMyFAQ release. - migrated codebase using PHP 8.4 language features (Thorsten) - migrated routes using PHP 8+ #[Route] attributes (Thorsten) - migrated to Vite v8 (Thorsten) +- migrated experimental MCP Server to mcp/sdk (Thorsten) ### phpMyFAQ v4.1.1 - unreleased diff --git a/composer.json b/composer.json index 7863815aa4..cc62504729 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "guzzlehttp/guzzle": "^7.5", "league/commonmark": "^2.4", "league/oauth2-server": "^9.2", + "mcp/sdk": "^0.4.0", "minishlink/web-push": "^10.0", "monolog/monolog": "^3.3", "myclabs/deep-copy": "~1.0", @@ -52,7 +53,6 @@ "symfony/http-foundation": "^8.0", "symfony/http-kernel": "^8.0", "symfony/mailer": "^8.0", - "symfony/mcp-sdk": "dev-main", "symfony/rate-limiter": "^8.0", "symfony/routing": "^8.0", "symfony/uid": "^8.0", @@ -65,7 +65,7 @@ "carthage-software/mago": "^1.0.1", "doctrine/instantiator": "2.*", "mikey179/vfsstream": "^1.6", - "phpdocumentor/reflection-docblock": "6.*", + "phpdocumentor/reflection-docblock": "^5.6", "phpunit/phpunit": "^12.3", "rector/rector": "^2", "symfony/browser-kit": "^8.0", diff --git a/composer.lock b/composer.lock index ac35f05984..6fba992779 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d34dd00c2f06422b12335651065d6353", + "content-hash": "4cab2d5e382f65d2c6b241fb5e99ffea", "packages": [ { "name": "2tvenom/cborencode", @@ -107,16 +107,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.373.5", + "version": "3.373.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "978964f417f3617a6ced691110af6fc5d496fb4e" + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/978964f417f3617a6ced691110af6fc5d496fb4e", - "reference": "978964f417f3617a6ced691110af6fc5d496fb4e", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/483fba51c28b3a0c0647bf5100e0edca82090b18", + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18", "shasum": "" }, "require": { @@ -198,9 +198,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.373.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.373.2" }, - "time": "2026-03-18T18:22:37+00:00" + "time": "2026-03-13T18:08:30+00:00" }, { "name": "bacon/bacon-qr-code", @@ -509,6 +509,54 @@ }, "time": "2024-07-08T12:26:09+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -1940,6 +1988,82 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "mcp/sdk", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/php-sdk.git", + "reference": "1f5f7e16a3af23dd43ec0a5c972d7aa8e8429024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/modelcontextprotocol/php-sdk/zipball/1f5f7e16a3af23dd43ec0a5c972d7aa8e8429024", + "reference": "1f5f7e16a3af23dd43ec0a5c972d7aa8e8429024", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "opis/json-schema": "^2.4", + "php": "^8.1", + "php-http/discovery": "^1.20", + "phpdocumentor/reflection-docblock": "^5.6", + "psr/clock": "^1.0", + "psr/container": "^1.0 || ^2.0", + "psr/event-dispatcher": "^1.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "require-dev": { + "laminas/laminas-httphandlerrunner": "^2.12", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phar-io/composer-distributor": "^1.0.2", + "php-cs-fixer/shim": "^3.91", + "phpdocumentor/shim": "^3", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "psr/simple-cache": "^2.0 || ^3.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0", + "symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mcp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Kyrian Obikwelu", + "email": "koshnawaza@gmail.com" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Model Context Protocol SDK for Client and Server applications in PHP", + "support": { + "issues": "https://github.com/modelcontextprotocol/php-sdk/issues", + "source": "https://github.com/modelcontextprotocol/php-sdk/tree/v0.4.0" + }, + "time": "2026-02-23T21:42:54+00:00" + }, { "name": "minishlink/web-push", "version": "v10.0.3", @@ -2612,6 +2736,196 @@ }, "time": "2026-01-16T21:44:15+00:00" }, + { + "name": "opis/json-schema", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/8458763e0dd0b6baa310e04f1829fc73da4e8c8a", + "reference": "8458763e0dd0b6baa310e04f1829fc73da4e8c8a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.1", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.6.0" + }, + "time": "2025-10-17T12:46:48+00:00" + }, + { + "name": "opis/string", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/3e4d2aaff518ac518530b89bb26ed40f4503635e", + "reference": "3e4d2aaff518ac518530b89bb26ed40f4503635e", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.1.0" + }, + "time": "2025-10-17T12:38:41+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.1.3", @@ -2920,41 +3234,31 @@ "time": "2024-03-15T13:55:21+00:00" }, { - "name": "phpseclib/phpseclib", - "version": "3.0.50", + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", - "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1|^2|^3", - "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": ">=5.6.1" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "suggest": { - "ext-dom": "Install the DOM extension to load XML formatted public keys.", - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + "php": "^7.2 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, "autoload": { - "files": [ - "phpseclib/bootstrap.php" - ], "psr-4": { - "phpseclib3\\": "phpseclib/" + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2963,10 +3267,195 @@ ], "authors": [ { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.6", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" + }, + "time": "2025-12-22T21:13:58+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + }, + "time": "2025-11-21T15:09:14+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.49", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, { "name": "Patrick Monnerat", "email": "pm@datasphere.ch", @@ -3011,7 +3500,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" }, "funding": [ { @@ -3027,7 +3516,54 @@ "type": "tidelift" } ], - "time": "2026-03-19T02:57:58+00:00" + "time": "2026-01-27T09:17:28+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" }, { "name": "psr/cache", @@ -4803,28 +5339,29 @@ "time": "2026-02-25T16:59:43+00:00" }, { - "name": "symfony/html-sanitizer", - "version": "v8.0.7", + "name": "symfony/finder", + "version": "v8.0.6", "source": { "type": "git", - "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2" + "url": "https://github.com/symfony/finder.git", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/555b37caeee3d07af33471e02377d5ff561f8ac2", - "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", "shasum": "" }, "require": { - "ext-dom": "*", - "league/uri": "^6.5|^7.0", "php": ">=8.4" }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HtmlSanitizer\\": "" + "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4836,23 +5373,18 @@ ], "authors": [ { - "name": "Titouan Galopin", - "email": "galopintitouan@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "Purifier", - "html", - "sanitizer" - ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.7" + "source": "https://github.com/symfony/finder/tree/v8.0.6" }, "funding": [ { @@ -4872,24 +5404,96 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:17:40+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { - "name": "symfony/http-client", + "name": "symfony/html-sanitizer", "version": "v8.0.7", "source": { "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "ade9bd433450382f0af154661fc8e72758b4de36" + "url": "https://github.com/symfony/html-sanitizer.git", + "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/ade9bd433450382f0af154661fc8e72758b4de36", - "reference": "ade9bd433450382f0af154661fc8e72758b4de36", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/555b37caeee3d07af33471e02377d5ff561f8ac2", + "reference": "555b37caeee3d07af33471e02377d5ff561f8ac2", "shasum": "" }, "require": { - "php": ">=8.4", + "ext-dom": "*", + "league/uri": "^6.5|^7.0", + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HtmlSanitizer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", + "homepage": "https://symfony.com", + "keywords": [ + "Purifier", + "html", + "sanitizer" + ], + "support": { + "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T13:17:40+00:00" + }, + { + "name": "symfony/http-client", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "ade9bd433450382f0af154661fc8e72758b4de36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/ade9bd433450382f0af154661fc8e72758b4de36", + "reference": "ade9bd433450382f0af154661fc8e72758b4de36", + "shasum": "" + }, + "require": { + "php": ">=8.4", "psr/log": "^1|^2|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" @@ -5401,80 +6005,6 @@ ], "time": "2026-02-25T16:59:43+00:00" }, - { - "name": "symfony/mcp-sdk", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/symfony/mcp-sdk.git", - "reference": "19464d5ce91b969c156d68ec7aa942e39ade5118" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/mcp-sdk/zipball/19464d5ce91b969c156d68ec7aa942e39ade5118", - "reference": "19464d5ce91b969c156d68ec7aa942e39ade5118", - "shasum": "" - }, - "require": { - "php": "^8.2", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/uid": "^7.3|^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^11.5", - "psr/cache": "^3.0", - "symfony/console": "^7.3|^8.0" - }, - "suggest": { - "psr/cache": "To use CachePoolStore with SSE Transport", - "symfony/console": "To use SymfonyConsoleTransport for STDIO" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\AI\\McpSdk\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christopher Hertel", - "email": "mail@christopher-hertel.de" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "description": "Model Context Protocol SDK for Client and Server applications in PHP", - "support": { - "source": "https://github.com/symfony/mcp-sdk/tree/main" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-09-18T06:25:22+00:00" - }, { "name": "symfony/mime", "version": "v8.0.7", @@ -7233,7 +7763,7 @@ }, { "name": "twig/intl-extra", - "version": "v3.24.0", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", @@ -7281,7 +7811,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.24.0" + "source": "https://github.com/twigphp/intl-extra/tree/v3.23.0" }, "funding": [ { @@ -7297,16 +7827,16 @@ }, { "name": "twig/twig", - "version": "v3.24.0", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a6769aefb305efef849dc25c9fd1653358c148f0" + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a6769aefb305efef849dc25c9fd1653358c148f0", - "reference": "a6769aefb305efef849dc25c9fd1653358c148f0", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", "shasum": "" }, "require": { @@ -7316,8 +7846,7 @@ "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "php-cs-fixer/shim": "^3.0@stable", - "phpstan/phpstan": "^2.0@stable", + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -7361,7 +7890,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.24.0" + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" }, "funding": [ { @@ -7373,7 +7902,7 @@ "type": "tidelift" } ], - "time": "2026-03-17T21:31:11+00:00" + "time": "2026-01-23T21:00:41+00:00" }, { "name": "web-token/jwt-library", @@ -7463,6 +7992,68 @@ } ], "time": "2025-12-18T14:27:35+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.6" + }, + "time": "2026-02-27T10:28:38+00:00" } ], "packages-dev": [ @@ -7522,54 +8113,6 @@ ], "time": "2026-03-14T00:50:47+00:00" }, - { - "name": "doctrine/deprecations", - "version": "1.1.6", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<=7.5 || >=14" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^14", - "phpstan/phpstan": "1.4.10 || 2.1.30", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.6" - }, - "time": "2026-02-07T07:09:04+00:00" - }, { "name": "doctrine/instantiator", "version": "2.1.0", @@ -7744,351 +8287,128 @@ "php" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" - }, - "time": "2025-12-06T11:56:16+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "6.0.3", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", - "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^2.0", - "phpstan/phpdoc-parser": "^2.0", - "webmozart/assert": "^1.9.1 || ^2" - }, - "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26", - "shipmonk/dead-code-detector": "^0.5.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2026-03-18T20:49:53+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "2.0.0", + "name": "phar-io/manifest", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", - "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^4" + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev", - "dev-2.x": "2.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2026-01-06T21:53:42+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.3.2", + "name": "phar-io/version", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", - "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2026-01-25T14:56:51+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpstan/phpstan", @@ -9877,74 +10197,6 @@ ], "time": "2026-02-17T13:07:04+00:00" }, - { - "name": "symfony/finder", - "version": "v8.0.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "shasum": "" - }, - "require": { - "php": ">=8.4" - }, - "require-dev": { - "symfony/filesystem": "^7.4|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-29T09:41:02+00:00" - }, { "name": "symfony/type-info", "version": "v8.0.7", @@ -10077,68 +10329,6 @@ ], "time": "2025-12-08T11:19:18+00:00" }, - { - "name": "webmozart/assert", - "version": "2.1.6", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-filter": "*", - "php": "^8.2" - }, - "suggest": { - "ext-intl": "", - "ext-simplexml": "", - "ext-spl": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-feature/2-0": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" - }, - "time": "2026-02-27T10:28:38+00:00" - }, { "name": "zircote/swagger-php", "version": "6.0.6", @@ -10232,9 +10422,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "symfony/mcp-sdk": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/docs/mcp-server.md b/docs/mcp-server.md index d86fbd018c..993f90121b 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -4,9 +4,11 @@ This document describes the Model Context Protocol (MCP) server implementation f ## 12.1 Overview -The phpMyFAQ MCP Server allows Large Language Models (LLMs) to query the phpMyFAQ installation through the Model +The phpMyFAQ MCP Server allows Large Language Models (LLMs) to query the phpMyFAQ installation through the Model Context Protocol. This enables AI assistants to provide contextually relevant answers based on your FAQ. +The current implementation uses the official PHP package `mcp/sdk` and exposes the server over STDIO. + ## 12.2 Usage ### 12.2.1 Starting the Server @@ -19,6 +21,8 @@ php bin/console phpmyfaq:mcp:server --info php bin/console phpmyfaq:mcp:server ``` +The command starts a STDIO MCP server. It is intended to be launched by an MCP-capable client or by the MCP Inspector. + ### 12.2.2 Available Tools #### faq_search @@ -44,15 +48,34 @@ Search through the phpMyFAQ knowledge base to find relevant FAQ entries. ``` **Response Format:** -The tool returns formatted text with FAQ entries including: +The tool returns JSON with: - Question and answer content - FAQ ID and language - Relevance score - Direct URL to the FAQ entry +Example result shape: + +```json +{ + "results": [ + { + "id": 42, + "language": "en", + "question": "How do I reset my password?", + "answer": "Use the password reset form on the login page.", + "category_id": 1, + "relevance_score": 0.95, + "url": "https://example.com/content/42/en" + } + ], + "total_found": 1 +} +``` + ## 12.3 Integration with LLM Clients -Once the MCP server is running, LLM clients can connect to it and use the `faq_search` tool to query your phpMyFAQ +Once the MCP server is running, LLM clients can connect to it and use the `faq_search` tool to query your phpMyFAQ database. The server follows the MCP specification and provides: - Tool discovery via `tools/list` @@ -84,7 +107,23 @@ You can then access the MCP Inspector at the provided URL to interact with the s ## 12.4 Configuration -No additional configuration is required beyond having a working phpMyFAQ installation. +No additional MCP-specific configuration is required beyond having a working phpMyFAQ installation. + +The MCP server uses the same phpMyFAQ runtime configuration as the rest of the application, including: + +- database configuration +- language configuration +- URL configuration used for generated FAQ links +- the standard logger + +## 12.4.1 Dependency Notes + +The server runtime depends on: + +- `mcp/sdk` +- `symfony/console` + +If you update dependencies, make sure `mcp/sdk` remains installable with the project Composer constraints. ## 12.5 Error Handling @@ -103,4 +142,17 @@ The server includes comprehensive error handling: ### 12.7.1 Debugging -Enable debug logging by checking the Monolog output when running the server. The logger outputs to stdout by default. +Enable debug logging by checking the Monolog output when running the server. + +### 12.7.2 Runtime Issues + +If the server does not start: + +- verify Composer dependencies are installed +- verify `mcp/sdk` is present in the installed packages +- run `php bin/console phpmyfaq:mcp:server --info` to confirm the command is wired correctly +- test the server with MCP Inspector before connecting a custom client + +### 12.7.3 Transport Notes + +The phpMyFAQ MCP server currently uses STDIO only. It is not exposed as HTTP transport by default. diff --git a/phpmyfaq/src/phpMyFAQ/Command/McpServerCommand.php b/phpmyfaq/src/phpMyFAQ/Command/McpServerCommand.php index 3cf345a3ac..7cb4be7fcd 100644 --- a/phpmyfaq/src/phpMyFAQ/Command/McpServerCommand.php +++ b/phpmyfaq/src/phpMyFAQ/Command/McpServerCommand.php @@ -20,7 +20,7 @@ namespace phpMyFAQ\Command; use Exception; -use phpMyFAQ\Service\McpServer\PhpMyFaqMcpServer; +use phpMyFAQ\Service\McpServer\McpServerRuntimeInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -39,7 +39,7 @@ class McpServerCommand extends Command { public function __construct( - private readonly PhpMyFaqMcpServer $phpMyFaqMcpServer, + private readonly McpServerRuntimeInterface $phpMyFaqMcpServer, ) { parent::__construct(); } diff --git a/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php b/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php index 857b4f7e95..3f08cd0323 100644 --- a/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php +++ b/phpmyfaq/src/phpMyFAQ/Controller/Administration/SystemInformationController.php @@ -107,6 +107,7 @@ public function index(Request $request): Response 'phpMyFAQ Version' => $faqSystem->getVersion(), 'phpMyFAQ API Version' => $faqSystem->getApiVersion(), 'phpMyFAQ Plugin API Version' => $faqSystem->getPluginVersion(), + 'phpMyFAQ MCP Server Version' => $faqSystem->getMcpServerVersion(), 'phpMyFAQ Installation Path' => dirname((string) $request->server->get('SCRIPT_FILENAME'), levels: 2), 'Web server software' => $request->server->get('SERVER_SOFTWARE'), 'Web server document root' => $request->server->get('DOCUMENT_ROOT'), diff --git a/phpmyfaq/src/phpMyFAQ/Search.php b/phpmyfaq/src/phpMyFAQ/Search.php index 089609e04f..a50846538d 100755 --- a/phpmyfaq/src/phpMyFAQ/Search.php +++ b/phpmyfaq/src/phpMyFAQ/Search.php @@ -54,9 +54,9 @@ public function __construct( /** * Setter for category. * - * @param int $categoryId Entity ID + * @param int|null $categoryId Entity ID */ - public function setCategoryId(int $categoryId): void + public function setCategoryId(?int $categoryId): void { $this->categoryId = $categoryId; } diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchTool.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchTool.php new file mode 100644 index 0000000000..9a238f791b --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchTool.php @@ -0,0 +1,187 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-03-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Service\McpServer; + +use Exception; +use phpMyFAQ\Category; +use phpMyFAQ\Configuration; +use phpMyFAQ\Faq; +use phpMyFAQ\Search; + +readonly class FaqSearchTool implements McpToolExecutorInterface +{ + public function __construct( + private Configuration $configuration, + private Search $search, + private Faq $faq, + ) { + } + + public function getDefinition(): McpToolDefinition + { + return new McpToolDefinition( + name: 'faq_search', + description: 'Search through the phpMyFAQ knowledge base to find relevant FAQ entries that can answer questions. ' + . 'This tool searches both questions and answers in the FAQ database to provide comprehensive results.', + title: 'FAQ Search', + inputSchema: [ + 'type' => 'object', + 'properties' => [ + 'query' => [ + 'type' => 'string', + 'description' => 'The search query or question to find relevant FAQ entries for', + ], + 'category_id' => [ + 'type' => 'integer', + 'description' => 'Optional category ID to limit search to a specific FAQ category', + 'minimum' => 1, + ], + 'limit' => [ + 'type' => 'integer', + 'description' => 'Maximum number of results to return (default: 10)', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 50, + ], + 'all_languages' => [ + 'type' => 'boolean', + 'description' => 'Whether to search in all languages or just the current language (default: false)', + 'default' => false, + ], + ], + 'required' => ['query'], + ], + outputSchema: [ + 'type' => 'object', + 'properties' => [ + 'results' => [ + 'type' => 'array', + 'description' => 'Array of FAQ search results', + ], + 'total_found' => [ + 'type' => 'integer', + 'description' => 'Total number of FAQ entries found', + ], + ], + ], + ); + } + + public function execute(array $arguments): array + { + $query = $arguments['query'] ?? ''; + $categoryId = $arguments['category_id'] ?? null; + $limit = $arguments['limit'] ?? 10; + $allLanguages = $arguments['all_languages'] ?? false; + + if (trim((string) $query) === '') { + return $this->createResult('Error: Search query cannot be empty.'); + } + + try { + $this->faq->setUser(-1); + $this->faq->setGroups([-1]); + + $category = new Category($this->configuration, [-1]); + $category->setUser(-1); + $this->search->setCategory($category); + + $this->search->setCategoryId($categoryId !== null ? (int) $categoryId : null); + + $searchResults = $this->search->search($query, (bool) $allLanguages); + + if ($searchResults === []) { + return $this->createResult($this->formatResultsAsJson([])); + } + + $validResults = []; + foreach ($searchResults as $searchResult) { + $validResults[] = [ + 'id' => $searchResult->id, + 'language' => $searchResult->lang, + 'question' => $searchResult->question ?? '', + 'answer' => $searchResult->answer ?? '', + 'category_id' => $searchResult->category_id ?? null, + 'relevance_score' => $searchResult->score ?? 0.0, + 'url' => $this->buildFaqUrl($searchResult->id, $searchResult->lang), + ]; + } + + $limitedResults = array_slice($validResults, 0, (int) $limit); + + if ($limitedResults === []) { + return $this->createResult('No accessible FAQ entries found for the given query.'); + } + + return $this->createResult($this->formatResultsAsJson($limitedResults)); + } catch (Exception $exception) { + return $this->createResult('Error searching FAQ database: ' . $exception->getMessage()); + } + } + + public function getSearch(): Search + { + return $this->search; + } + + public function getFaq(): Faq + { + return $this->faq; + } + + /** + * @param array> $results + */ + private function formatResultsAsJson(array $results): string + { + if ($results === []) { + $jsonData = [ + 'results' => [], + 'total_found' => 0, + ]; + + return json_encode($jsonData); + } + + $jsonData = [ + 'results' => $results, + 'total_found' => count($results), + ]; + + return json_encode($jsonData, JSON_PRETTY_PRINT); + } + + private function buildFaqUrl(int $faqId, string $language): string + { + return $this->configuration->getDefaultUrl() . 'content/' . $faqId . '/' . $language; + } + + /** + * @return array{content: string, type: string, mimeType: string} + */ + private function createResult(string $content): array + { + return [ + 'content' => $content, + 'type' => 'text', + 'mimeType' => 'application/json', + ]; + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolExecutor.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolExecutor.php deleted file mode 100644 index faf6ada6dc..0000000000 --- a/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolExecutor.php +++ /dev/null @@ -1,148 +0,0 @@ - - * @copyright 2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2025-08-16 - */ - -declare(strict_types=1); - -namespace phpMyFAQ\Service\McpServer; - -use Exception; -use phpMyFAQ\Category; -use phpMyFAQ\Configuration; -use phpMyFAQ\Faq; -use phpMyFAQ\Search; -use Symfony\AI\McpSdk\Capability\Tool\IdentifierInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolCall; -use Symfony\AI\McpSdk\Capability\Tool\ToolCallResult; -use Symfony\AI\McpSdk\Capability\Tool\ToolExecutorInterface; - -/** - * Class FaqSearchToolExecutor - * - * Executes the FAQ search functionality for the MCP server. - * This class uses phpMyFAQ's Search and Faq classes to search - * through the knowledge base and return relevant FAQ entries. - */ -readonly class FaqSearchToolExecutor implements ToolExecutorInterface, IdentifierInterface -{ - public function __construct( - private Configuration $configuration, - private Search $search, - private Faq $faq, - ) { - } - - public function getName(): string - { - return 'faq_search'; - } - - /** - * @throws Exception - */ - public function call(ToolCall $toolCall): ToolCallResult - { - $query = $toolCall->arguments['query'] ?? ''; - $categoryId = $toolCall->arguments['category_id'] ?? null; - $limit = $toolCall->arguments['limit'] ?? 10; - $allLanguages = $toolCall->arguments['all_languages'] ?? false; - - if (trim((string) $query) === '') { - return new ToolCallResult('Error: Search query cannot be empty.', 'text', 'application/json'); - } - - try { - $this->faq->setUser(-1); - $this->faq->setGroups([-1]); - - // Set the category class - $category = new Category($this->configuration, [-1]); - $category->setUser(-1); - $this->search->setCategory($category); - - // Set category filter if provided - if ($categoryId !== null) { - $this->search->setCategoryId((int) $categoryId); - } - - // Perform the search - $searchResults = $this->search->search($query, (bool) $allLanguages); - - if ($searchResults === []) { - $emptyResult = $this->formatResultsAsJson([]); - return new ToolCallResult($emptyResult, 'text', 'application/json'); - } - - // Format the results - $validResults = []; - foreach ($searchResults as $searchResult) { - $this->configuration->getLogger()->info(var_export($searchResult, return: true)); - - $validResults[] = [ - 'id' => $searchResult->id, - 'language' => $searchResult->lang, - 'question' => $searchResult->question ?? '', - 'answer' => $searchResult->answer ?? '', - 'category_id' => $searchResult->category_id ?? null, - 'relevance_score' => $searchResult->score ?? 0.0, - 'url' => $this->buildFaqUrl($searchResult->id, $searchResult->lang), - ]; - } - - // Limit results - $limitedResults = array_slice($validResults, offset: 0, length: (int) $limit); - - if ($limitedResults === []) { - return new ToolCallResult( - 'No accessible FAQ entries found for the given query.', - 'text', - 'application/json', - ); - } - - $resultJson = $this->formatResultsAsJson($limitedResults); - return new ToolCallResult($resultJson, 'text', 'application/json'); - } catch (Exception $exception) { - return new ToolCallResult( - 'Error searching FAQ database: ' . $exception->getMessage(), - 'text', - 'application/json', - ); - } - } - - private function formatResultsAsJson(array $results): string - { - if ($results === []) { - return json_encode([ - 'results' => [], - 'total_found' => 0, - ]); - } - - $jsonData = [ - 'results' => $results, - 'total_found' => count($results), - ]; - - return json_encode($jsonData, JSON_PRETTY_PRINT); - } - - private function buildFaqUrl(int $faqId, string $language): string - { - return $this->configuration->getDefaultUrl() . 'content/' . $faqId . '/' . $language; - } -} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolMetadata.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolMetadata.php deleted file mode 100644 index 6409ca6c2e..0000000000 --- a/phpmyfaq/src/phpMyFAQ/Service/McpServer/FaqSearchToolMetadata.php +++ /dev/null @@ -1,104 +0,0 @@ - - * @copyright 2025 phpMyFAQ Team - * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 - * @link https://www.phpmyfaq.de - * @since 2025-08-16 - */ - -declare(strict_types=1); - -namespace phpMyFAQ\Service\McpServer; - -use Symfony\AI\McpSdk\Capability\Tool\MetadataInterface; -use Symfony\AI\McpSdk\Capability\Tool\ToolAnnotationsInterface; - -/** - * Class FaqSearchToolMetadata - * - * Defines the metadata for the FAQ search tool in the MCP server. - * This tool allows LLMs to search through phpMyFAQ's knowledge base - * to find relevant FAQ entries based on user questions. - */ -class FaqSearchToolMetadata implements MetadataInterface -{ - public function getName(): string - { - return 'faq_search'; - } - - public function getDescription(): ?string - { - return ( - 'Search through the phpMyFAQ knowledge base to find relevant FAQ entries that can answer questions. ' - . 'This tool searches both questions and answers in the FAQ database to provide comprehensive results.' - ); - } - - public function getTitle(): ?string - { - return 'FAQ Search'; - } - - public function getInputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'query' => [ - 'type' => 'string', - 'description' => 'The search query or question to find relevant FAQ entries for', - ], - 'category_id' => [ - 'type' => 'integer', - 'description' => 'Optional category ID to limit search to a specific FAQ category', - 'minimum' => 1, - ], - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum number of results to return (default: 10)', - 'default' => 10, - 'minimum' => 1, - 'maximum' => 50, - ], - 'all_languages' => [ - 'type' => 'boolean', - 'description' => 'Whether to search in all languages or just the current language (default: false)', - 'default' => false, - ], - ], - 'required' => ['query'], - ]; - } - - public function getOutputSchema(): ?array - { - return [ - 'type' => 'object', - 'properties' => [ - 'results' => [ - 'type' => 'array', - 'description' => 'Array of FAQ search results', - ], - 'total_found' => [ - 'type' => 'integer', - 'description' => 'Total number of FAQ entries found', - ], - ], - ]; - } - - public function getAnnotations(): ?ToolAnnotationsInterface - { - return null; - } -} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpSdkRuntime.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpSdkRuntime.php new file mode 100644 index 0000000000..a9c0c6f164 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpSdkRuntime.php @@ -0,0 +1,108 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-03-18 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Service\McpServer; + +use phpMyFAQ\Configuration; +use RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +final class McpSdkRuntime implements McpServerRuntimeInterface +{ + /** + * @param array $serverInfo + */ + public function __construct( + private readonly Configuration $configuration, + private readonly FaqSearchTool $faqSearchTool, + private readonly array $serverInfo, + ) { + } + + public function runConsole(InputInterface $input, OutputInterface $output): void + { + $this->buildServer()->run(new \Mcp\Server\Transport\StdioTransport()); + } + + public function getServerInfo(): array + { + return $this->serverInfo; + } + + /** + * Adapter method for mcp/sdk manual tool registration. + * + * Returns the decoded JSON payload for successful searches and a string for + * user-facing errors, matching the intent of the current Symfony MCP runtime. + * + * @return array|string + */ + public function faqSearch( + string $query, + ?int $category_id = null, + int $limit = 10, + bool $all_languages = false, + ): array|string { + $result = $this->faqSearchTool->execute([ + 'query' => $query, + 'category_id' => $category_id, + 'limit' => $limit, + 'all_languages' => $all_languages, + ]); + + $decoded = json_decode($result['content'], true); + + return json_last_error() === JSON_ERROR_NONE ? $decoded : $result['content']; + } + + private function buildServer(): \Mcp\Server + { + if (!class_exists(\Mcp\Server::class) || !class_exists(\Mcp\Server\Transport\StdioTransport::class)) { + throw new RuntimeException( + 'The mcp/sdk package is not installed or does not expose the expected server classes.', + ); + } + + $definition = $this->faqSearchTool->getDefinition(); + + return \Mcp\Server::builder() + ->setServerInfo( + (string) $this->serverInfo['name'], + (string) $this->serverInfo['version'], + (string) ($this->serverInfo['description'] ?? null), + ) + ->addTool( + fn( + string $query, + ?int $category_id = null, + int $limit = 10, + bool $all_languages = false, + ): array|string => $this->faqSearch($query, $category_id, $limit, $all_languages), + $definition->name, + $definition->description, + null, + $definition->inputSchema, + null, + null, + $definition->outputSchema, + ) + ->build(); + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpServerRuntimeInterface.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpServerRuntimeInterface.php new file mode 100644 index 0000000000..0c6de57fb2 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpServerRuntimeInterface.php @@ -0,0 +1,33 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-03-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Service\McpServer; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +interface McpServerRuntimeInterface +{ + public function runConsole(InputInterface $input, OutputInterface $output): void; + + /** + * @return array + */ + public function getServerInfo(): array; +} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolDefinition.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolDefinition.php new file mode 100644 index 0000000000..e06af53a6f --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolDefinition.php @@ -0,0 +1,38 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-03-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Service\McpServer; + +readonly class McpToolDefinition +{ + /** + * @param array $inputSchema + * @param array|null $outputSchema + * @param array $annotations + */ + public function __construct( + public string $name, + public string $description, + public ?string $title, + public array $inputSchema, + public ?array $outputSchema = null, + public array $annotations = [], + ) { + } +} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolExecutorInterface.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolExecutorInterface.php new file mode 100644 index 0000000000..c427a7b357 --- /dev/null +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/McpToolExecutorInterface.php @@ -0,0 +1,31 @@ + + * @copyright 2026 phpMyFAQ Team + * @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0 + * @link https://www.phpmyfaq.de + * @since 2026-03-16 + */ + +declare(strict_types=1); + +namespace phpMyFAQ\Service\McpServer; + +interface McpToolExecutorInterface +{ + public function getDefinition(): McpToolDefinition; + + /** + * @param array $arguments + * @return array{content: string, type: string, mimeType: string} + */ + public function execute(array $arguments): array; +} diff --git a/phpmyfaq/src/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServer.php b/phpmyfaq/src/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServer.php index 2db0c7f190..b874e76b88 100644 --- a/phpmyfaq/src/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServer.php +++ b/phpmyfaq/src/phpMyFAQ/Service/McpServer/PhpMyFaqMcpServer.php @@ -23,14 +23,7 @@ use phpMyFAQ\Faq; use phpMyFAQ\Language; use phpMyFAQ\Search; -use Symfony\AI\McpSdk\Capability\ToolChain; -use Symfony\AI\McpSdk\Message\Factory; -use Symfony\AI\McpSdk\Server; -use Symfony\AI\McpSdk\Server\JsonRpcHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\InitializeHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolCallHandler; -use Symfony\AI\McpSdk\Server\RequestHandler\ToolListHandler; -use Symfony\AI\McpSdk\Server\Transport\Stdio\SymfonyConsoleTransport; +use phpMyFAQ\System; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -41,57 +34,40 @@ * with FAQ search capabilities. This allows LLM models to query the phpMyFAQ knowledge base * through the MCP protocol. */ -class PhpMyFaqMcpServer +class PhpMyFaqMcpServer implements McpServerRuntimeInterface { - private JsonRpcHandler $jsonRpcHandler; + private McpServerRuntimeInterface $runtime; private const string MCP_SERVER_NAME = 'phpMyFAQ MCP Server'; - private const string MCP_SERVER_VERSION = '0.1.0-dev'; - public function __construct( private readonly Configuration $configuration, Language $language, private readonly Search $search, private readonly Faq $faq, + ?McpServerRuntimeInterface $runtime = null, ) { $detectionEnabled = (bool) $this->configuration->get(item: 'main.languageDetection'); $configLang = (string) $this->configuration->get(item: 'main.language'); if ($detectionEnabled) { $language->setLanguageWithDetection($configLang); $this->configuration->setLanguage($language); - $this->initializeServer(); + $this->initializeServer($runtime); return; } $language->setLanguageFromConfiguration($configLang); $this->configuration->setLanguage($language); - $this->initializeServer(); + $this->initializeServer($runtime); } - private function initializeServer(): void + private function initializeServer(?McpServerRuntimeInterface $runtime = null): void { - $toolChain = new ToolChain([ - new FaqSearchToolMetadata(), - new FaqSearchToolExecutor($this->configuration, $this->search, $this->faq), - ]); - - $messageFactory = new Factory(); - - $requestHandlers = [ - new InitializeHandler(self::MCP_SERVER_NAME, self::MCP_SERVER_VERSION), - new ToolListHandler($toolChain), - new ToolCallHandler($toolChain), - ]; - - $notificationHandlers = []; - - $this->jsonRpcHandler = new JsonRpcHandler( - $messageFactory, - $requestHandlers, - $notificationHandlers, - $this->configuration->getLogger(), + $this->runtime = $runtime ?? new McpSdkRuntime( + $this->configuration, + new FaqSearchTool($this->configuration, $this->search, $this->faq), + $this->createServerInfo(), ); } @@ -100,27 +76,25 @@ private function initializeServer(): void */ public function runConsole(InputInterface $input, OutputInterface $output): void { - $symfonyConsoleTransport = new SymfonyConsoleTransport($input, $output); - $server = new Server($this->jsonRpcHandler, $this->configuration->getLogger()); - $server->connect($symfonyConsoleTransport); + $this->runtime->runConsole($input, $output); } /** - * Get the configured JSON-RPC handler + * Get server information for debugging */ - public function getJsonRpcHandler(): JsonRpcHandler + public function getServerInfo(): array { - return $this->jsonRpcHandler; + return $this->runtime->getServerInfo(); } /** - * Get server information for debugging + * @return array */ - public function getServerInfo(): array + private function createServerInfo(): array { return [ 'name' => self::MCP_SERVER_NAME, - 'version' => self::MCP_SERVER_VERSION, + 'version' => System::getMcpServerVersion(), 'description' => 'Model Context Protocol server for phpMyFAQ installations', 'capabilities' => [ 'tools' => true, diff --git a/phpmyfaq/src/phpMyFAQ/System.php b/phpmyfaq/src/phpMyFAQ/System.php index 90f27940b8..db52f6017b 100644 --- a/phpmyfaq/src/phpMyFAQ/System.php +++ b/phpmyfaq/src/phpMyFAQ/System.php @@ -66,6 +66,11 @@ class System */ private const string PLUGIN_VERSION = '0.2.0'; + /** + * MCP Server version + */ + private const string MCP_SERVER_VERSION = '0.1.0'; + /** * Minimum required PHP version. */ @@ -189,6 +194,14 @@ public static function getPluginVersion(): string return self::PLUGIN_VERSION; } + /** + * Returns the current MCP Server version of phpMyFAQ + */ + public static function getMcpServerVersion(): string + { + return self::MCP_SERVER_VERSION; + } + public static function getPoweredByString(): string { return 'powered with ❤️ and ☕️ by