Skip to content

Commit 6e9e88d

Browse files
authored
feat(laravel): mcp support (#7709)
1 parent 2ad839d commit 6e9e88d

File tree

14 files changed

+991
-54
lines changed

14 files changed

+991
-54
lines changed

src/Laravel/ApiPlatformDeferredProvider.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
5050
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
5151
use ApiPlatform\Laravel\State\ValidateProvider;
52+
use ApiPlatform\Mcp\State\ToolProvider;
5253
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
5354
use ApiPlatform\Metadata\InflectorInterface;
5455
use ApiPlatform\Metadata\Laravel\SkipAutoconfigure;
@@ -90,6 +91,7 @@
9091
use Illuminate\Support\ServiceProvider;
9192
use Negotiation\Negotiator;
9293
use Psr\Log\LoggerInterface;
94+
use Symfony\Component\ObjectMapper\ObjectMapper;
9395
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
9496
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
9597

@@ -198,7 +200,15 @@ public function register(): void
198200
return new CallableProvider(new ServiceLocator($tagged));
199201
});
200202

201-
$this->autoconfigure($classes, ProviderInterface::class, [ItemProvider::class, CollectionProvider::class, ErrorProvider::class]);
203+
if (class_exists(ToolProvider::class)) {
204+
$this->app->singleton(ToolProvider::class, static function (Application $app) {
205+
return new ToolProvider(
206+
$app->make(ObjectMapper::class)
207+
);
208+
});
209+
}
210+
211+
$this->autoconfigure($classes, ProviderInterface::class, [ItemProvider::class, CollectionProvider::class, ErrorProvider::class, ToolProvider::class]);
202212

203213
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) {
204214
/** @var ConfigRepository $config */
@@ -366,6 +376,7 @@ public function provides(): array
366376
'api_platform.graphql.state_provider.parameter',
367377
FieldsBuilderEnumInterface::class,
368378
ExceptionHandlerInterface::class,
379+
ToolProvider::class,
369380
];
370381
}
371382
}

src/Laravel/ApiPlatformProvider.php

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
107107
use ApiPlatform\Laravel\State\SwaggerUiProvider;
108108
use ApiPlatform\Laravel\State\ValidateProvider;
109+
use ApiPlatform\Mcp\Capability\Registry\Loader as McpLoader;
110+
use ApiPlatform\Mcp\Metadata\Operation\Factory\OperationMetadataFactory as McpOperationMetadataFactory;
111+
use ApiPlatform\Mcp\Routing\IriConverter as McpIriConverter;
112+
use ApiPlatform\Mcp\Server\Handler;
113+
use ApiPlatform\Mcp\State\StructuredContentProcessor;
109114
use ApiPlatform\Metadata\IdentifiersExtractor;
110115
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
111116
use ApiPlatform\Metadata\InflectorInterface;
@@ -147,7 +152,6 @@
147152
use ApiPlatform\State\CallableProcessor;
148153
use ApiPlatform\State\CallableProvider;
149154
use ApiPlatform\State\ErrorProvider;
150-
use ApiPlatform\State\ObjectMapper\ObjectMapper;
151155
use ApiPlatform\State\Pagination\Pagination;
152156
use ApiPlatform\State\Pagination\PaginationOptions;
153157
use ApiPlatform\State\Processor\AddLinkHeaderProcessor;
@@ -164,15 +168,25 @@
164168
use ApiPlatform\State\Provider\ReadProvider;
165169
use ApiPlatform\State\ProviderInterface;
166170
use ApiPlatform\State\SerializerContextBuilderInterface;
171+
use Http\Discovery\Psr17Factory;
167172
use Illuminate\Config\Repository as ConfigRepository;
168173
use Illuminate\Contracts\Foundation\Application;
169174
use Illuminate\Routing\Router;
170175
use Illuminate\Support\ServiceProvider;
176+
use Mcp\Capability\Registry;
177+
use Mcp\Server;
178+
use Mcp\Server\Builder;
179+
use Mcp\Server\Session\InMemorySessionStore;
171180
use Negotiation\Negotiator;
172181
use PHPStan\PhpDocParser\Parser\PhpDocParser;
173182
use Psr\Log\LoggerInterface;
183+
use Symfony\AI\McpBundle\Controller\McpController;
184+
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
185+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
186+
use Symfony\Component\HttpFoundation\RequestStack;
174187
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
175188
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory;
189+
use Symfony\Component\ObjectMapper\ObjectMapper;
176190
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
177191
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
178192
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
@@ -427,11 +441,9 @@ public function register(): void
427441
$this->app->singleton(ObjectMapperMetadataFactoryInterface::class, ReflectionObjectMapperMetadataFactory::class);
428442

429443
$this->app->singleton(ObjectMapper::class, static function (Application $app) {
430-
if (!$app->bound('api_platform.object_mapper')) {
431-
return null;
432-
}
433-
434-
return new ObjectMapper($app->make('api_platform.object_mapper'));
444+
return new ObjectMapper(
445+
$app->make(ObjectMapperMetadataFactoryInterface::class)
446+
);
435447
});
436448

437449
$this->app->extend(ProviderInterface::class, static function (ProviderInterface $inner, Application $app) {
@@ -891,6 +903,8 @@ public function register(): void
891903
$this->registerGraphQl();
892904
}
893905

906+
$this->registerMcp();
907+
894908
$this->app->singleton(JsonApiEntrypointNormalizer::class, static function (Application $app) {
895909
return new JsonApiEntrypointNormalizer(
896910
$app->make(ResourceMetadataCollectionFactoryInterface::class),
@@ -1039,6 +1053,126 @@ public function register(): void
10391053
}
10401054
}
10411055

1056+
private function registerMcp(): void
1057+
{
1058+
if (!class_exists(McpController::class)) {
1059+
return;
1060+
}
1061+
1062+
$this->app->singleton(Registry::class, static function (Application $app) {
1063+
return new Registry(
1064+
null, // event dispatcher (todo)
1065+
$app->make(LoggerInterface::class)
1066+
);
1067+
});
1068+
1069+
$this->app->singleton(McpLoader::class, static function (Application $app) {
1070+
return new McpLoader(
1071+
$app->make(ResourceNameCollectionFactoryInterface::class),
1072+
$app->make(ResourceMetadataCollectionFactoryInterface::class),
1073+
$app->make(SchemaFactoryInterface::class)
1074+
);
1075+
});
1076+
$this->app->tag(McpLoader::class, 'mcp.loader');
1077+
1078+
// TODO: add more stores?
1079+
$this->app->singleton('mcp.session.store', static function () {
1080+
return new InMemorySessionStore(3600);
1081+
});
1082+
1083+
$this->app->singleton(Builder::class, static function (Application $app) {
1084+
$config = $app['config'];
1085+
1086+
$builder = Server::builder()
1087+
->setServerInfo(
1088+
$config->get('api-platform.title', 'API Platform'),
1089+
$config->get('api-platform.version', '1.0.0'),
1090+
$config->get('api-platform.description', 'My awesome API'),
1091+
[], // icons
1092+
null // website_url todo
1093+
)
1094+
->setPaginationLimit(100)
1095+
->setRegistry($app->make(Registry::class))
1096+
->setSession($app->make('mcp.session.store'));
1097+
1098+
foreach ($app->tagged('mcp.loader') as $loader) {
1099+
$builder->addLoader($loader);
1100+
}
1101+
1102+
$builder->addRequestHandler($app->make(Handler::class));
1103+
1104+
return $builder;
1105+
});
1106+
1107+
$this->app->singleton(Server::class, static function (Application $app) {
1108+
return $app->make(Builder::class)->build();
1109+
});
1110+
1111+
$this->app->singleton(McpOperationMetadataFactory::class, static function (Application $app) {
1112+
return new McpOperationMetadataFactory(
1113+
$app->make(ResourceNameCollectionFactoryInterface::class),
1114+
$app->make(ResourceMetadataCollectionFactoryInterface::class)
1115+
);
1116+
});
1117+
1118+
$this->app->singleton('api_platform.mcp.state_processor.write', static function (Application $app) {
1119+
return new WriteProcessor(
1120+
null,
1121+
$app->make(CallableProcessor::class)
1122+
);
1123+
});
1124+
1125+
$this->app->singleton(StructuredContentProcessor::class, static function (Application $app) {
1126+
return new StructuredContentProcessor(
1127+
$app->make(SerializerInterface::class),
1128+
$app->make(SerializerContextBuilderInterface::class),
1129+
$app->make('api_platform.mcp.state_processor.write')
1130+
);
1131+
});
1132+
1133+
$this->app->singleton(RequestStack::class, static function (Application $app) {
1134+
return new RequestStack();
1135+
});
1136+
1137+
$this->app->singleton(Handler::class, static function (Application $app) {
1138+
return new Handler(
1139+
$app->make(McpOperationMetadataFactory::class),
1140+
$app->make(ProviderInterface::class),
1141+
$app->make(StructuredContentProcessor::class),
1142+
$app->make(RequestStack::class),
1143+
$app->make(LoggerInterface::class)
1144+
);
1145+
});
1146+
1147+
$this->app->extend(IriConverterInterface::class, static function (IriConverterInterface $iriConverter) {
1148+
return new McpIriConverter($iriConverter);
1149+
});
1150+
1151+
// Register Symfony MCP controller
1152+
$this->app->singleton(McpController::class, static function (Application $app) {
1153+
if (!class_exists('Http\Discovery\Psr17Factory')) {
1154+
throw new \RuntimeException('PSR-17 HTTP factory implementation not available. Please install php-http/discovery.');
1155+
}
1156+
1157+
$psr17Factory = new Psr17Factory();
1158+
$psrHttpFactory = new PsrHttpFactory(
1159+
$psr17Factory,
1160+
$psr17Factory,
1161+
$psr17Factory,
1162+
$psr17Factory
1163+
);
1164+
$httpFoundationFactory = new HttpFoundationFactory();
1165+
1166+
return new McpController(
1167+
$app->make(Server::class),
1168+
$psrHttpFactory,
1169+
$httpFoundationFactory,
1170+
$psr17Factory,
1171+
$psr17Factory
1172+
);
1173+
});
1174+
}
1175+
10421176
private function registerGraphQl(): void
10431177
{
10441178
$this->app->singleton(GraphQlItemNormalizer::class, static function (Application $app) {

src/Laravel/Eloquent/Serializer/SerializerClassMetadataFactory.php.bak

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)