From 95ed90d65c35a0d7377d1c41687c3b78399f186c Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Tue, 21 Jul 2026 16:12:34 +0000 Subject: [PATCH 1/4] fix(metadata): preserve bracketed indexes in query values --- src/Metadata/Tests/Util/IriHelperTest.php | 23 ++++++++++++++++-- src/Metadata/Util/IriHelper.php | 29 +++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Metadata/Tests/Util/IriHelperTest.php b/src/Metadata/Tests/Util/IriHelperTest.php index ead4540ed23..5593e1d174a 100644 --- a/src/Metadata/Tests/Util/IriHelperTest.php +++ b/src/Metadata/Tests/Util/IriHelperTest.php @@ -23,6 +23,10 @@ */ class IriHelperTest extends TestCase { + private const OBJECTS_PATH = '/objects'; + private const PAGE_PARAMETER_NAME = 'page'; + private const BRACKETED_INDEX_VALUE = 'x[0]'; + public function testHelpers(): void { $parsed = [ @@ -93,18 +97,33 @@ public function testHelpersPreserveNestedArrayQueryParameters(): void public function testHelpersCollapseSimpleListQueryParameters(): void { $parts = [ - 'path' => '/objects', + 'path' => self::OBJECTS_PATH, 'query' => '', ]; $parameters = [ 'foo' => ['a', 'b'], ]; - $iri = IriHelper::createIri($parts, $parameters, 'page', 2.); + $iri = IriHelper::createIri($parts, $parameters, self::PAGE_PARAMETER_NAME, 2.); $this->assertSame('/objects?foo%5B%5D=a&foo%5B%5D=b&page=2', $iri); } + public function testCreateIriPreservesBracketedNumericIndexesInQueryValues(): void + { + $parts = [ + 'path' => self::OBJECTS_PATH, + 'query' => '', + ]; + $parameters = [ + 'v' => self::BRACKETED_INDEX_VALUE, + ]; + + $iri = IriHelper::createIri($parts, $parameters, self::PAGE_PARAMETER_NAME, 2.); + + $this->assertSame('/objects?v=x%5B0%5D&page=2', $iri); + } + public function testParseIriWithInvalidUrl(): void { $this->expectException(InvalidArgumentException::class); diff --git a/src/Metadata/Util/IriHelper.php b/src/Metadata/Util/IriHelper.php index 61c2d61f888..d79fbbee26f 100644 --- a/src/Metadata/Util/IriHelper.php +++ b/src/Metadata/Util/IriHelper.php @@ -26,6 +26,11 @@ */ final class IriHelper { + private const EMPTY_BRACKET_REPLACEMENT = '%5B%5D'; + private const INDEXED_LEAF_BRACKET_PATTERN = '/%5B\d+%5D(?!%5B)/'; + private const QUERY_ASSIGNMENT_SEPARATOR = '='; + private const QUERY_SEPARATOR = '&'; + private function __construct() { } @@ -64,11 +69,8 @@ public static function createIri(array $parts, array $parameters, ?string $pageP $parameters[$pageParameterName] = $page; } - $query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986); - // Only collapse a numeric index when it is the leaf segment of a bracket chain - // (a simple list element). Collapsing a non-leaf index would merge distinct keys of a - // nested array into separate elements (e.g. filters[0][a] must stay filters[0][a]). - $parts['query'] = preg_replace('/%5B\d+%5D(?!%5B)/', '%5B%5D', $query); + $query = http_build_query($parameters, '', self::QUERY_SEPARATOR, \PHP_QUERY_RFC3986); + $parts['query'] = self::collapseNumericLeafIndexesInQueryKeys($query); $url = ''; if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) { @@ -110,4 +112,21 @@ public static function createIri(array $parts, array $parameters, ?string $pageP return $url; } + + private static function collapseNumericLeafIndexesInQueryKeys(string $query): string + { + $parameters = explode(self::QUERY_SEPARATOR, $query); + + foreach ($parameters as $index => $parameter) { + [$key, $value] = explode(self::QUERY_ASSIGNMENT_SEPARATOR, $parameter, 2) + [1 => null]; + + // Only collapse a numeric index when it is the leaf segment of a bracket chain + // (a simple list element). Collapsing a non-leaf index would merge distinct keys of a + // nested array into separate elements (e.g. filters[0][a] must stay filters[0][a]). + $key = preg_replace(self::INDEXED_LEAF_BRACKET_PATTERN, self::EMPTY_BRACKET_REPLACEMENT, $key); + $parameters[$index] = null === $value ? $key : $key.self::QUERY_ASSIGNMENT_SEPARATOR.$value; + } + + return implode(self::QUERY_SEPARATOR, $parameters); + } } From 02aea9dcd82cbf39b9f289e2bb221697902c7c33 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Tue, 21 Jul 2026 16:26:54 +0000 Subject: [PATCH 2/4] ci: allow mcp sdk 0.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e1b841d1530..4772ec8fc31 100644 --- a/composer.json +++ b/composer.json @@ -142,7 +142,7 @@ "jangregor/phpstan-prophecy": "^2.1.11", "justinrainbow/json-schema": "^6.5.2", "laravel/framework": "^11.0 || ^12.0 || ^13.0", - "mcp/sdk": "^0.6", + "mcp/sdk": "^0.7", "orchestra/testbench": "^10.9 || ^11.0", "phpspec/prophecy-phpunit": "^2.2", "phpstan/extension-installer": "^1.1", From 6cbd099cca925be6f491590120f07b7a50655937 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Tue, 21 Jul 2026 16:29:43 +0000 Subject: [PATCH 3/4] ci: align mcp package sdk constraint --- src/Mcp/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mcp/composer.json b/src/Mcp/composer.json index a824d20b63d..608d3f2425b 100644 --- a/src/Mcp/composer.json +++ b/src/Mcp/composer.json @@ -30,7 +30,7 @@ "php": ">=8.2", "api-platform/metadata": "^5.0@alpha", "api-platform/json-schema": "^5.0@alpha", - "mcp/sdk": "^0.6", + "mcp/sdk": "^0.7", "symfony/object-mapper": "^7.4 || ^8.0", "symfony/polyfill-php85": "^1.32" }, From aca3107a20367d86890dcdfed1c364b8a275601b Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Tue, 21 Jul 2026 19:33:30 +0000 Subject: [PATCH 4/4] ci: adapt laravel mcp controller wiring --- src/Laravel/ApiPlatformProvider.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 4cc6fd7aa1f..aaca8987c0f 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -193,6 +193,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use Psr\Log\LoggerInterface; use Symfony\AI\McpBundle\Controller\McpController; +use Symfony\AI\McpBundle\Http\MiddlewareFactory; use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Component\HttpFoundation\RequestStack; @@ -1342,7 +1343,8 @@ private function registerMcp(): void $psrHttpFactory, $httpFoundationFactory, $psr17Factory, - $psr17Factory + $psr17Factory, + new MiddlewareFactory() ); }); }