Integration of Model Context Protocol (MCP) for Nette Framework.
Install package using composer.
composer require contributte/mcpRegister prepared compiler extension in your config.neon file.
extensions:
mcp: Contributte\Mcp\DI\McpExtensionThe simplest configuration requires only a server name:
mcp:
servers:
default:
name: My MCP Server
version: 1.0.0Here is the list of all available configuration options:
mcp:
servers:
<server-name>:
# Server information
name: <string> # Server name (default: 'MCP')
version: <string> # Server version (default: '1.0.0')
# Discovery configuration
discovery:
enabled: <bool> # Enable auto-discovery (default: true)
basePath: <string> # Base path for discovery (default: %appDir%)
scanDirs: <array<string>> # Directories to scan relative to basePath (default: ['.'])
excludeDirs: <array<string>> # Directories to exclude (default: [])
cache: <service|null> # PSR-16 cache service for discovery results
# Session configuration
session:
type: <'file'|'inmemory'|'psr16'> # Session type (default: 'file' if %tempDir% exists, otherwise 'inmemory')
path: <string|null> # Path for file sessions
ttl: <int> # Time to live in seconds (default: 3600)
prefix: <string> # Session key prefix (default: 'mcp-')
cache: <service|null> # PSR-16 cache service for sessions
# Container integration
container: <service|class-name|null> # PSR-11 container implementation (default: NetteContainer adapter)Each server configuration defines an MCP server instance. You can define multiple servers with different names:
mcp:
servers:
default:
name: Main MCP Server
version: 1.0.0
secondary:
name: Secondary MCP Server
version: 2.0.0Use McpManager to get server factories:
use Contributte\Mcp\McpManager;
class MyService
{
public function __construct(
private McpManager $mcpManager
) {
}
public function doMagic(): void
{
$server = $this->mcpManager
->getServerFactory('secondary')
->create();
// Do something with server
}
}Discovery is enabled by default and automatically scans your application for MCP tools, resources, and prompts using PHP attributes.
mcp:
servers:
default:
name: My MCP Server
version: 1.0.0
discovery:
enabled: true # Default: true
basePath: %appDir% # Default: %appDir%
scanDirs:
- src/Mcp # Directories relative to basePath
excludeDirs:
- vendor
- tests
cache: @cacheService # Optional: PSR-16 cache for discovery resultsThe discovery mechanism scans PHP files in the specified directories and automatically registers classes with MCP attributes (#[McpTool], #[McpResource], #[McpPrompt], #[McpResourceTemplate]).
Tip
Use a PSR-16 cache service to improve performance in production environments, as discovery scanning can be expensive.
MCP servers can maintain session state. Three session types are supported:
File-based sessions (default):
mcp:
servers:
default:
session:
type: file
path: %tempDir%/mcp-sessions
ttl: 3600In-memory sessions:
mcp:
servers:
default:
session:
type: inmemory
ttl: 3600PSR-16 cache sessions:
mcp:
servers:
default:
session:
type: psr16
cache: @psr16CacheService
ttl: 3600
prefix: mcp-Note
The prefix option only applies to PSR-16 cache sessions. It is ignored for file-based and in-memory sessions.
The extension automatically provides a NetteContainer adapter out-of-the-box, which enables dependency injection in MCP tools using the Nette DI container. No configuration is required.
If you need to use a custom PSR-11 container implementation instead, you can override it:
mcp:
servers:
default:
container: @myContainerServiceThe extension registers two transport factories by default:
stdio- STDIN/STDOUT transport for command-line usagestreamable- HTTP streamable transport for web requests
// Get stdio transport factory
$stdioFactory = $mcpManager->getTransportFactory('stdio');
// Get streamable transport factory
$streamableFactory = $mcpManager->getTransportFactory('streamable');You can create custom transport factories by implementing TransportFactoryInterface:
<?php declare(strict_types=1);
namespace App\Mcp;
use Contributte\Mcp\Http\TransportFactoryInterface;
use Mcp\Server\Transport\TransportInterface;
final class CustomTransportFactory implements TransportFactoryInterface
{
public function create(mixed ...$args): TransportInterface
{
return new MyCustomTransport();
}
}Register it in your configuration:
services:
mcp.transport.custom.factory:
factory: App\Mcp\CustomTransportFactory
tags: [contributte.mcp.transport_factory: custom]
# Now you can use it
# $factory = $mcpManager->getTransportFactory('custom');MCP capabilities (tools, resources, prompts) are automatically discovered using PHP attributes. There is no manual configuration needed - just create classes with the appropriate attributes and they will be registered automatically.
Tools are methods that can be called by MCP clients. Use the #[McpTool] attribute:
<?php declare(strict_types=1);
namespace App\Mcp;
use Mcp\Capability\Attribute\McpTool;
final class CalculatorTool
{
#[McpTool(name: 'calculator_add', description: 'Add two numbers')]
public function add(int $a, int $b): int
{
return $a + $b;
}
#[McpTool(name: 'calculator_multiply', description: 'Multiply two numbers')]
public function multiply(int $a, int $b): int
{
return $a * $b;
}
}You can also use the attribute on a class with an __invoke method:
<?php declare(strict_types=1);
namespace App\Mcp;
use Mcp\Capability\Attribute\McpTool;
#[McpTool(name: 'weather', description: 'Get current weather for a location')]
final class WeatherTool
{
public function __invoke(string $location): array
{
// Fetch weather data...
return ['temperature' => 22, 'condition' => 'sunny'];
}
}Resources provide data that MCP clients can read. Use the #[McpResource] attribute:
<?php declare(strict_types=1);
namespace App\Mcp;
use Mcp\Capability\Attribute\McpResource;
final class ConfigResource
{
#[McpResource(uri: 'app://config', name: 'App Config', description: 'Application configuration')]
public function getConfig(): string
{
return json_encode(['version' => '1.0.0', 'env' => 'production']);
}
#[McpResource(uri: 'file://readme', name: 'Readme', description: 'Project readme file', mimeType: 'text/markdown')]
public function getReadme(): string
{
return file_get_contents(__DIR__ . '/../../README.md');
}
}Resource templates define URI patterns with placeholders. Use the #[McpResourceTemplate] attribute:
<?php declare(strict_types=1);
namespace App\Mcp;
use Mcp\Capability\Attribute\McpResourceTemplate;
final class UserResource
{
#[McpResourceTemplate(uriTemplate: 'user://{id}', name: 'User', description: 'Get user by ID')]
public function getUser(string $id): string
{
$user = $this->userRepository->find($id);
return json_encode($user);
}
}Prompts are predefined templates that MCP clients can use. Use the #[McpPrompt] attribute:
<?php declare(strict_types=1);
namespace App\Mcp;
use Mcp\Capability\Attribute\McpPrompt;
final class CodePrompts
{
#[McpPrompt(name: 'code_review', description: 'Review code for best practices')]
public function codeReview(string $code, string $language): string
{
return "Please review this {$language} code for best practices:\n\n```{$language}\n{$code}\n```";
}
#[McpPrompt(name: 'explain_code', description: 'Explain what code does')]
public function explainCode(string $code): string
{
return "Please explain what this code does:\n\n```\n{$code}\n```";
}
}All discovered MCP classes support dependency injection through the Nette DI container:
<?php declare(strict_types=1);
namespace App\Mcp;
use App\Model\UserRepository;
use Mcp\Capability\Attribute\McpTool;
final class UserTool
{
public function __construct(
private UserRepository $userRepository
) {
}
#[McpTool(name: 'find_user', description: 'Find user by email')]
public function findByEmail(string $email): array
{
$user = $this->userRepository->findByEmail($email);
return $user ? $user->toArray() : [];
}
}After configuration, you can use the MCP server in your application:
<?php declare(strict_types=1);
namespace App\Presenters;
use Contributte\Mcp\Http\GuzzleBridge;
use Contributte\Mcp\McpManager;
use Nette\Application\IPresenter;
use Nette\Application\Request as AppRequest;
use Nette\Application\Response;
use Nette\Http\IRequest;
use Psr\Http\Message\ResponseInterface;
class McpPresenter implements IPresenter
{
public function __construct(
protected IRequest $httpRequest,
protected McpManager $mcpManager,
)
{
}
public function run(AppRequest $appRequest): Response
{
// Get server name from route parameters
$serverName = $appRequest->getParameter('server');
$serverName = is_string($serverName) ? $serverName : 'default';
// Convert Nette request to PSR-7 request
$serverRequest = GuzzleBridge::fromNette($this->httpRequest);
// Create server and transport
$server = $this->mcpManager->getServerFactory($serverName)->create();
$transport = $this->mcpManager->getTransportFactory('streamable')->create($serverRequest);
// Run server
$psr7Response = $server->run($transport);
assert($psr7Response instanceof ResponseInterface);
return GuzzleBridge::toNette($psr7Response);
}
}<?php declare(strict_types=1);
use Contributte\Mcp\McpManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class McpController
{
public function __construct(
private readonly McpManager $mcpManager
) {
}
public function __invoke(ServerRequestInterface $serverRequest): ResponseInterface
{
$server = $this->mcpManager->getServerFactory('default')->create();
$transport = $this->mcpManager->getTransportFactory('streamable')->create($serverRequest);
$response = $server->run($transport);
return $response;
}
}When Tracy debugger is enabled, a debug panel is automatically registered showing all registered tools, resources, resource templates, and prompts along with their handlers.
To inspect the MCP server configuration without processing actual MCP requests, add the ?debug=1 query parameter to your MCP endpoint URL:
https://your-app.com/mcp?debug=1
This will return a simple text response and allow you to inspect the Tracy debug bar to see:
- Number of registered tools, resources, templates, and prompts
- Handler class and method for each registered item (e.g.,
App\Mcp\CalculatorTool::add()) - Tool descriptions and input schemas
- Resource URIs and MIME types
- Prompt arguments
# config.neon
extensions:
mcp: Contributte\Mcp\DI\McpExtension
mcp:
servers:
default:
name: My Application MCP Server
version: 1.0.0
discovery:
scanDirs:
- src/Mcp<?php declare(strict_types=1);
// src/Mcp/CalculatorTool.php
namespace App\Mcp;
use Mcp\Capability\Attribute\McpTool;
final class CalculatorTool
{
#[McpTool(description: 'Add two numbers')]
public function add(int $a, int $b): int
{
return $a + $b;
}
#[McpTool(name: 'multiply', description: 'Multiply two numbers')]
public function multiplyNumbers(int $a, int $b): int
{
return $a * $b;
}
}mcp:
servers:
public:
name: Public MCP Server
version: 1.0.0
discovery:
scanDirs:
- src/Mcp/Public
internal:
name: Internal MCP Server
version: 1.0.0
discovery:
scanDirs:
- src/Mcp/Internal<?php declare(strict_types=1);
namespace App\Presenters;
use Contributte\Mcp\Http\GuzzleBridge;
use Contributte\Mcp\McpManager;
use Nette\Application\IPresenter;
use Nette\Application\Request as AppRequest;
use Nette\Application\Response;
use Nette\Http\IRequest;
use Psr\Http\Message\ResponseInterface;
class McpPresenter implements IPresenter
{
public function __construct(
protected IRequest $httpRequest,
protected McpManager $mcpManager,
)
{
}
public function run(AppRequest $appRequest): Response
{
// Get server name from route parameters
$serverName = $appRequest->getParameter('server');
$serverName = is_string($serverName) ? $serverName : 'default';
// Convert Nette request to PSR-7 request
$serverRequest = GuzzleBridge::fromNette($this->httpRequest);
// Create server and transport
$server = $this->mcpManager->getServerFactory($serverName)->create();
$transport = $this->mcpManager->getTransportFactory('streamable')->create($serverRequest);
// Run server
$psr7Response = $server->run($transport);
assert($psr7Response instanceof ResponseInterface);
return GuzzleBridge::toNette($psr7Response);
}
}<?php declare(strict_types=1);
namespace App\Controllers;
use Contributte\Mcp\McpManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class McpController
{
public function __construct(
private readonly McpManager $mcpManager
) {
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Create server and transport
$server = $this->mcpManager->getServerFactory('default')->create();
$transport = $this->mcpManager->getTransportFactory('streamable')->create($request);
// Run server and return PSR-7 response
return $server->run($transport);
}
}Create a standalone mcp.php script:
#!/usr/bin/env php
<?php declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Contributte\Mcp\McpManager;
$container = require __DIR__ . '/app/bootstrap.php';
/** @var McpManager $mcpManager */
$mcpManager = $container->getByType(McpManager::class);
// Create server
$server = $mcpManager->getServerFactory('default')->create();
// Create stdio transport
$transport = $mcpManager->getTransportFactory('stdio')->create();
// Run server
$server->run($transport);Make it executable:
chmod +x mcp.phpRun it:
./mcp.phpThe package provides a built-in Symfony console command Contributte\Mcp\Console\McpCommand for running MCP servers.
Register it in your console configuration (if using contributte/console):
services:
- Contributte\Mcp\Console\McpCommandRun the command:
php bin/console mcp:server --server=defaultOr specify a different server:
php bin/console mcp:server --server=secondaryTip
For more information about MCP, visit the official MCP documentation.
For information about the underlying PHP SDK, see the MCP PHP SDK documentation.
