Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,31 @@ public function load(array $configs, ContainerBuilder $container): void
// McpToolProvider requires Symfony's object_mapper service; mirror FrameworkBundle's gate so we don't try to wire it when object-mapper is dev-only.
if (($config['mcp']['enabled'] ?? false) && class_exists(McpBundle::class) && ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) {
$loader->load('mcp/mcp.php');
$loader->load($config['use_symfony_listeners'] ? 'mcp/events.php' : 'mcp/state.php');

if ($config['use_symfony_listeners']) {
// In this mode the state pipeline is driven by kernel listeners, which never run for
// a JSON-RPC tool call, so MCP needs its own provider chain to keep enforcing
// security, parameters and validation.
$loader->load('mcp/events.php');

/** @var string[] $bundles */
$bundles = $container->getParameter('kernel.bundles');
$hasValidator = interface_exists(ValidatorInterface::class);

if ($hasValidator) {
$loader->load('mcp/validator.php');
}

if (isset($bundles['SecurityBundle'])) {
$loader->load('mcp/security.php');

if ($hasValidator) {
$loader->load('mcp/security_validator.php');
}
}
} else {
$loader->load('mcp/state.php');
}
}

$container->registerForAutoconfiguration(FilterInterface::class)
Expand Down
45 changes: 44 additions & 1 deletion src/Symfony/Bundle/Resources/config/mcp/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,53 @@
use ApiPlatform\Mcp\Server\Handler;
use ApiPlatform\Mcp\State\StructuredContentProcessor;
use ApiPlatform\State\Processor\WriteProcessor;
use ApiPlatform\State\Provider\ContentNegotiationProvider;
use ApiPlatform\State\Provider\DeserializeProvider;
use ApiPlatform\State\Provider\ParameterProvider;
use ApiPlatform\State\Provider\ReadProvider;

return static function (ContainerConfigurator $container) {
$services = $container->services();

// A tool call is dispatched over JSON-RPC from within the handler, never as an HTTP request
// cycle, so the kernel listeners that build the state pipeline in this mode never fire. MCP
// therefore gets its own provider chain, mirroring "api_platform.state_provider.main" so that
// security, parameters and validation behave the same whichever mode is configured.
// See mcp/security.php, mcp/security_validator.php and mcp/validator.php for the decorators.
$services->alias('api_platform.mcp.state_provider', 'api_platform.state_provider.locator');

$services->set('api_platform.mcp.state_provider.read', ReadProvider::class)
->decorate('api_platform.mcp.state_provider', null, 500)
->args([
service('api_platform.mcp.state_provider.read.inner'),
service('api_platform.serializer.context_builder'),
]);

$services->set('api_platform.mcp.state_provider.deserialize', DeserializeProvider::class)
->decorate('api_platform.mcp.state_provider', null, 300)
->args([
service('api_platform.mcp.state_provider.deserialize.inner'),
service('api_platform.serializer'),
service('api_platform.serializer.context_builder'),
service('translator')->nullOnInvalid(),
]);

$services->set('api_platform.mcp.state_provider.parameter', ParameterProvider::class)
->decorate('api_platform.mcp.state_provider', null, 180)
->args([
service('api_platform.mcp.state_provider.parameter.inner'),
tagged_locator('api_platform.parameter_provider', 'key'),
]);

$services->set('api_platform.mcp.state_provider.content_negotiation', ContentNegotiationProvider::class)
->decorate('api_platform.mcp.state_provider', null, 100)
->args([
service('api_platform.mcp.state_provider.content_negotiation.inner'),
service('api_platform.negotiator'),
'%api_platform.formats%',
'%api_platform.error_formats%',
]);

$services->set('api_platform.mcp.state_processor.write', WriteProcessor::class)
->args([
null,
Expand All @@ -36,7 +79,7 @@
$services->set('api_platform.mcp.handler', Handler::class)
->args([
service('api_platform.mcp.metadata.operation.mcp_factory'),
service('api_platform.state_provider.locator'),
service('api_platform.mcp.state_provider'),
service('api_platform.mcp.state_processor'),
service('request_stack'),
service('logger')->ignoreOnInvalid(),
Expand Down
51 changes: 51 additions & 0 deletions src/Symfony/Bundle/Resources/config/mcp/security.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use ApiPlatform\State\Provider\SecurityParameterProvider;
use ApiPlatform\Symfony\Security\State\AccessCheckerProvider;

return static function (ContainerConfigurator $container) {
$services = $container->services();

$services->set('api_platform.mcp.state_provider.access_checker', AccessCheckerProvider::class)
->decorate('api_platform.mcp.state_provider.read', null, 0)
->args([
service('api_platform.mcp.state_provider.access_checker.inner'),
service('api_platform.security.resource_access_checker'),
]);

$services->set('api_platform.mcp.state_provider.access_checker.post_deserialize', AccessCheckerProvider::class)
->decorate('api_platform.mcp.state_provider.deserialize', null, 0)
->args([
service('api_platform.mcp.state_provider.access_checker.post_deserialize.inner'),
service('api_platform.security.resource_access_checker'),
'post_denormalize',
]);

$services->set('api_platform.mcp.state_provider.security_parameter', SecurityParameterProvider::class)
->decorate('api_platform.mcp.state_provider.access_checker', null, 0)
->args([
service('api_platform.mcp.state_provider.security_parameter.inner'),
service('api_platform.security.resource_access_checker'),
]);

$services->set('api_platform.mcp.state_provider.access_checker.pre_read', AccessCheckerProvider::class)
->decorate('api_platform.mcp.state_provider.read', null, 10)
->args([
service('api_platform.mcp.state_provider.access_checker.pre_read.inner'),
service('api_platform.security.resource_access_checker'),
'pre_read',
]);
};
28 changes: 28 additions & 0 deletions src/Symfony/Bundle/Resources/config/mcp/security_validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use ApiPlatform\Symfony\Security\State\AccessCheckerProvider;

return static function (ContainerConfigurator $container) {
$services = $container->services();

$services->set('api_platform.mcp.state_provider.access_checker.post_validate', AccessCheckerProvider::class)
->decorate('api_platform.mcp.state_provider.validate', null, 0)
->args([
service('api_platform.mcp.state_provider.access_checker.post_validate.inner'),
service('api_platform.security.resource_access_checker'),
'post_validate',
]);
};
6 changes: 5 additions & 1 deletion src/Symfony/Bundle/Resources/config/mcp/state.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
return static function (ContainerConfigurator $container) {
$services = $container->services();

// The main provider chain already carries read, deserialize, validate, parameters and the
// access checkers, so MCP reuses it as-is.
$services->alias('api_platform.mcp.state_provider', 'api_platform.state_provider.main');

$services->set('api_platform.mcp.state_processor.write', WriteProcessor::class)
->args([
null,
Expand All @@ -36,7 +40,7 @@
$services->set('api_platform.mcp.handler', Handler::class)
->args([
service('api_platform.mcp.metadata.operation.mcp_factory'),
service('api_platform.state_provider.main'),
service('api_platform.mcp.state_provider'),
service('api_platform.mcp.state_processor'),
service('request_stack'),
service('logger')->ignoreOnInvalid(),
Expand Down
35 changes: 35 additions & 0 deletions src/Symfony/Bundle/Resources/config/mcp/validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use ApiPlatform\Symfony\Validator\State\ParameterValidatorProvider;
use ApiPlatform\Symfony\Validator\State\ValidateProvider;

return static function (ContainerConfigurator $container) {
$services = $container->services();

$services->set('api_platform.mcp.state_provider.validate', ValidateProvider::class)
->decorate('api_platform.mcp.state_provider', null, 200)
->args([
service('api_platform.mcp.state_provider.validate.inner'),
service('api_platform.validator'),
]);

$services->set('api_platform.mcp.state_provider.parameter_validator', ParameterValidatorProvider::class)
->decorate('api_platform.mcp.state_provider', null, 191)
->args([
service('validator'),
service('api_platform.mcp.state_provider.parameter_validator.inner'),
]);
};
48 changes: 48 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/McpSecuredReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Operation;

/**
* Link target of the "secured_uri_variable_tool" of {@see McpSecuredTools}: link security first
* reads the related resource, then evaluates the expression carried by the Link.
*/
#[ApiResource(
shortName: 'McpSecuredReference',
operations: [
new Get(
uriTemplate: '/mcp_secured_references/{reference}',
uriVariables: ['reference'],
provider: [self::class, 'provide']
),
]
)]
class McpSecuredReference
{
public function __construct(
public ?string $reference = null,
) {
}

/**
* @param array<string, mixed> $uriVariables
*/
public static function provide(Operation $operation, array $uriVariables = []): self
{
return new self($uriVariables['reference'] ?? null);
}
}
79 changes: 79 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/McpSecuredTools.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\McpTool;

#[ApiResource(
shortName: 'McpSecuredTools',
operations: [],
mcp: [
'secured_tool' => new McpTool(
security: "is_granted('ROLE_ADMIN')",
processor: [self::class, 'process']
),
'secured_post_denormalize_tool' => new McpTool(
securityPostDenormalize: "is_granted('ROLE_ADMIN')",
processor: [self::class, 'process']
),
'secured_post_validation_tool' => new McpTool(
validate: true,
securityPostValidation: "is_granted('ROLE_ADMIN')",
processor: [self::class, 'process']
),
'secured_uri_variable_tool' => new McpTool(
uriVariables: [
'reference' => new Link(fromClass: McpSecuredReference::class, security: "is_granted('ROLE_ADMIN')"),
],
processor: [self::class, 'process']
),
]
)]
class McpSecuredTools
{
public function __construct(
private ?string $text = null,
private ?string $reference = null,
) {
}

public function getText(): ?string
{
return $this->text;
}

public function setText(?string $text): void
{
$this->text = $text;
}

public function getReference(): ?string
{
return $this->reference;
}

public function setReference(?string $reference): void
{
$this->reference = $reference;
}

public static function process($data): mixed
{
$data->setText('Secured: '.$data->getText());

return $data;
}
}
Loading
Loading