Architecture
McpServerFactory: from tool classes to an SDK Server
McpServerFactory::create() (src/McpServerFactory.php) is the single place that turns application config into an Mcp\Server\Builder-built Server:
- For every FQCN in
tools, it reflects the class's public methods for the SDK's own attributes (#[McpTool],#[McpResource],#[McpResourceTemplate],#[McpPrompt]) and registers each as a[class, method]handler viaBuilder::addTool()/addResource()/addResourceTemplate()/addPrompt(). A class implementingConditionalToolInterfaceis resolved through the container first and skipped whenshouldRegister()returnsfalse. A class with no capability attributes on any public method throwsInvalidToolClassExceptionat build time — never a silently empty registration. - Every
ServerConfiguratorInterface(the Markdown-prompts configurator, the OpenAPI bridge, your own) runs next, each contributing capabilities to the same builder. A configurator implementingReservedToolNamesAwareInterfaceis handed the attribute tools' names first, so it can fail fast on a collision instead of silently losing a tool to the SDK's last-write-wins registry (see below). - If any tool/prompt/resource interceptor or visibility filter is configured, the builder's reference handler is swapped for
Interceptor\InterceptingReferenceHandler— a decorator wrapping the SDK's ownReferenceHandler, so every registration path (attribute tools, OpenAPI-bridged operations, configurator-added handlers) goes through the same chain. With nothing configured, the SDK's reference handler is used unmodified — zero overhead. - The registry is always wrapped in
GuardedRegistry(@internal): the SDK registry is last-write-wins with no duplicate check, so a name collision between any two registration paths would otherwise silently drop one handler.GuardedRegistrythrowsException\DuplicateCapabilityExceptionat build time on any live duplicate — this is the second, structural line of defense behind the reserved-names handshake in step 2. - If any per-session visibility filter is configured, the corresponding
Filtered*Handler(Visibility\FilteredListToolsHandler,FilteredListPromptsHandler,FilteredListResourcesHandler,FilteredListResourceTemplatesHandler, andFilteredCompletionCompleteHandlerwhen prompt or resource visibility is set) is registered as a request handler ahead of the SDK's own — see Visibility for whycompletion/completeneeds its own decorator.
Wiring: config/di.php + config/params.php
Everything above is driven from one params namespace, params['rasuvaeff/yii3-mcp'], read once in config/di.php when the Server service is built:
SessionStoreInterfaceis bound toSession\PrivateFileSessionStore(owner-only, FPM-safe) by default — see Security.McpServerFactoryreceivesserver_name,server_version,instructions,pagination_limit, and aprotocol_versionresolved at config-load time: an unsupported value throws immediately, not on the first request.- The
Serverdefinition closure assembles, in order: the Markdown-prompts configurator (ifprompts_pathis set), the OpenAPI bridge configurator (ifopenapi.spec_pathandopenapi.operationsare both non-empty), the MCP Apps configurator (ifapps.enableorapps.definitionsis set), then every FQCN inconfigurators; the interceptor list (session budget →interceptors→ caching → size limit, see Interceptors); and the visibility bindings (tool_visibilityor declarativevisibility,prompt_visibility,resource_visibility). McpActionreceives the builtServer, PSR-17 factories,allowed_hosts(for the transport's DNS-rebinding protection), and the sameSessionStoreInterface— required for session-ownership enforcement (see Security).SharedSecretMiddlewarereceivesendpoint_secret(or aStaticSecretResolverbuilt fromclient_secrets) andsecret_header.McpDoctor(behindmcp:doctor) receives enough of the same config to diagnose it independently — see Operations.
Everything is resolved through the container inside these closures ($container->get($interceptorClass)), not eagerly at config-parse time — so an interceptor, visibility class, or identity provider is only instantiated when it is actually configured.
What's a PSR service vs. what's package config
McpServerFactory and McpAction depend only on PSR interfaces (ContainerInterface, ResponseFactoryInterface, StreamFactoryInterface, SessionStoreInterface from mcp/sdk) — see Framework-agnostic usage for wiring the same classes outside Yii3 entirely. config/di.php and config/params.php are the yiisoft/config-plugin convenience layer specific to this package; they do not add a Yii3 runtime dependency to the classes themselves.
Different entry points need different PSR services present in the container:
| Entry point / feature | Required services |
|---|---|
McpAction | ResponseFactoryInterface, StreamFactoryInterface |
McpListCommand, McpTester | ServerRequestFactoryInterface, ResponseFactoryInterface, StreamFactoryInterface |
| URL OpenAPI spec | PSR-18 ClientInterface, PSR-17 RequestFactoryInterface |
| OpenAPI operation execution | ClientInterface, RequestFactoryInterface, StreamFactoryInterface |
| URL spec cache | PSR-16 CacheInterface, when openapi.cache_ttl > 0 |
ServerRequestFactoryInterface and RequestFactoryInterface are distinct PSR-17 contracts — binding one does not satisfy the other. Keep these bound in every config group that builds Mcp\Server, including the console group used by mcp:list/mcp:doctor/mcp:serve — mcp:doctor reports a missing service by its exact interface name.