The Model Context Protocol (MCP) enables AI assistants to interact directly with your applications, but most examples focus on JavaScript/TypeScript. If you're a PHP developer using Symfony, you might be curious how to leverage MCP in your projects. This tutorial shows you exactly how. You'll build a complete MCP server that gives Claude and other AI assistants the ability to query your database, analyse customer data, and interact with your Symfony application through human language.
The outcome of this tutorial will add MCP server capabilities to a Symfony Customer Management application, allowing AI assistants like Claude to:
- Search for customers by name, email, city, or county
- Get detailed customer information with order history
- Analyse order data and customer statistics
- Query recent orders and products
Prerequisites
To follow this tutorial you'll need:
- PHP 8.1+ installed,
- Composer installed,
- Git installed locally to clone the repository and checkout to the starter branch.
Get Started
The first step of this tutorial is to clone the Github repository and checkout to the starter branch. I've already built a Symfony application with some demo customer accounts, orders etc. So, first run the following commands to clone the repository and change directory into the application:
git clone git@github.com:GregHolmes/build-a-symfony-mcp-server-tutorial.git
cd build-a-symfony-mcp-server-tutorial
You should by default be checkedout to the starter branch, but if you're not, checkout to the starter branch. The starter branch contains the complete Symfony application without MCP:
git checkout starter
Install the dependencies with the single composer install command:
composer install
Let's make sure the starter branch is working as expected by starting up the server:
php -S localhost:8000 -t public
When you visit http://localhost:8000 you'll see:
- 100 UK customers with realistic data
- Orders with status badges
- Products organized by category
Below are two screenshots showing two of the pages you should expect to see when loading the page.
The first image shows the page displaying 100 customer records:
While the second shows a table of 100 order records by customers:
If you're interested in the data you're seeing, it's being fed from the SQLite database at var/data.db.
The Database Structure
Entities
The application has four main entities with relationships. This represents a data model in your application and maps to a database table:
Customer (src/Entity/Customer.php)
- Properties:
firstName,lastName,email,phone,address,city, state (county),zipCode - Relationships: Each customer can have many Orders.
Order (src/Entity/Order.php)
- Properties:
orderDate,status,total - Relationships: Each order belongs to a Customer and has many OrderItems.
OrderItem (src/Entity/OrderItem.php)
- Properties:
quantity,price - Relationships: Each OrderItem belongs to Order and Product
Product (src/Entity/Product.php)
- Properties:
name,description,price,category
Repositories
This tutorial has four custom query methods are available in repository classes:
-
CustomerRepository::findBySearchTerm()- Search customers -
CustomerRepository::findByState()- Find customers by county -
CustomerRepository::findTopSpenders()- Get top spending customers -
OrderRepository::findRecentOrders()- Get recent orders
Install the Symfony MCP SDK
The Symfony MCP SDK enables AI assistants like Claude to directly interact with your Symfony application's data and functionality through a standardized protocol, allowing you to build AI-powered interfaces without creating custom APIs.
Important: The Symfony MCP SDK is currently experimental and does not yet have a stable release. The API may change in future versions.
Install the official MCP SDK package in your application with the following command:
composer require mcp/sdk
Note: The MCP SDK requires Symfony 6.4+. The starter branch already has this 7.3, so the SDK will install without issues.
You can check that the package is installed by running composer show:
composer show mcp/sdk
Create MCP Tools
MCP servers expose "tools" that AI assistants can call. We'll create tools using PHP attributes.
MCP services define the tools that AI assistants can call. Each public method marked with the #[McpTool] attribute becomes a callable tool. Our CustomerMcpService will expose five tools for customer search, order analysis, and data queries.
Create src/Service/CustomerMcpService.php:
<?php
namespace App\Service;
use App\Repository\CustomerRepository;
use App\Repository\OrderRepository;
use App\Repository\ProductRepository;
use Mcp\Capability\Attribute\McpTool;
class CustomerMcpService
{
public function __construct(
private CustomerRepository $customerRepository,
private OrderRepository $orderRepository,
private ProductRepository $productRepository,
) {}
#[McpTool(
name: 'search_customers',
description: 'Search for customers by name, email, city, or county'
)]
public function searchCustomers(string $searchTerm): array
{
$customers = $this->customerRepository->findBySearchTerm($searchTerm);
return array_map(fn($customer) => [
'id' => $customer->getId(),
'name' => $customer->getFullName(),
'email' => $customer->getEmail(),
'city' => $customer->getCity(),
'county' => $customer->getState(),
'totalOrders' => $customer->getOrders()->count(),
'totalSpent' => $customer->getTotalSpent(),
], $customers);
}
#[McpTool(
name: 'get_customer_details',
description: 'Get detailed information about a specific customer including order history'
)]
public function getCustomerDetails(int $customerId): array
{
$customer = $this->customerRepository->find($customerId);
if (!$customer) {
return ['error' => 'Customer not found'];
}
$orders = [];
foreach ($customer->getOrders() as $order) {
$orders[] = [
'id' => $order->getId(),
'date' => $order->getOrderDate()->format('Y-m-d'),
'status' => $order->getStatus(),
'total' => $order->getTotal(),
'itemCount' => $order->getOrderItems()->count(),
];
}
return [
'id' => $customer->getId(),
'name' => $customer->getFullName(),
'email' => $customer->getEmail(),
'phone' => $customer->getPhone(),
'address' => $customer->getAddress(),
'city' => $customer->getCity(),
'county' => $customer->getState(),
'postcode' => $customer->getZipCode(),
'customerSince' => $customer->getCreatedAt()->format('Y-m-d'),
'totalOrders' => count($orders),
'totalSpent' => $customer->getTotalSpent(),
'orders' => $orders,
];
}
#[McpTool(
name: 'get_recent_orders',
description: 'Get a list of recent orders with customer information'
)]
public function getRecentOrders(int $limit = 20): array
{
$orders = $this->orderRepository->findRecentOrders($limit);
return array_map(fn($order) => [
'id' => $order->getId(),
'date' => $order->getOrderDate()->format('Y-m-d'),
'customer' => $order->getCustomer()->getFullName(),
'customerId' => $order->getCustomer()->getId(),
'status' => $order->getStatus(),
'total' => $order->getTotal(),
'itemCount' => $order->getOrderItems()->count(),
], $orders);
}
#[McpTool(
name: 'get_customers_by_county',
description: 'Find all customers in a specific UK county'
)]
public function getCustomersByCounty(string $county): array
{
$customers = $this->customerRepository->findByState($county);
return array_map(fn($customer) => [
'id' => $customer->getId(),
'name' => $customer->getFullName(),
'email' => $customer->getEmail(),
'city' => $customer->getCity(),
'totalOrders' => $customer->getOrders()->count(),
], $customers);
}
#[McpTool(
name: 'get_top_spenders',
description: 'Get the top spending customers'
)]
public function getTopSpenders(int $limit = 10): array
{
$customers = $this->customerRepository->findTopSpenders($limit);
return array_map(fn($customer) => [
'id' => $customer->getId(),
'name' => $customer->getFullName(),
'totalSpent' => $customer->getTotalSpent(),
'totalOrders' => $customer->getOrders()->count(),
'city' => $customer->getCity(),
'county' => $customer->getState(),
], $customers);
}
}
Register the Service
The service needs to be made public so it can be accessed from the MCP server script. Update config/services.yaml and add the public service declaration at the end of the file (after the App\: namespace declaration):
services:
_defaults:
autowire: true
autoconfigure: true
# makes classes in src/ available to be used as services
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# Make CustomerMcpService public so it can be accessed from the MCP server script
App\Service\CustomerMcpService:
public: true
Important: The
App\Service\CustomerMcpServicedeclaration must come after theApp\:namespace declaration. In Symfony's service configuration, the last definition wins, so placing it at the end ensures it overrides the default private visibility.
After updating the configuration, clear the cache:
php bin/console cache:clear
Create the MCP Server
The MCP server is the bridge between AI assistants and your Symfony application. It listens for requests from Claude (or other AI assistants), routes those requests to the appropriate tools in your service class, and returns the results. This server script uses STDIO (standard input/output) to communicate using the MCP protocol and handles all the protocol details so your service methods can focus on business logic.
Create a new file in the bin directory called mcp-server.php:
#!/usr/bin/env php
<?php
use App\Kernel;
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
use Symfony\Component\Dotenv\Dotenv;
require_once dirname(__DIR__).'/vendor/autoload.php';
// Load environment variables
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
// Boot Symfony kernel to get the service container
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', (bool) ($_SERVER['APP_DEBUG'] ?? true));
$kernel->boot();
$container = $kernel->getContainer();
// Get the CustomerMcpService from Symfony's container
$customerService = $container->get('App\Service\CustomerMcpService');
// Build the MCP server and register the service methods as tools using closures
$server = Server::builder()
->setServerInfo('Symfony Customer Manager', '1.0.0')
->addTool(fn(string $searchTerm) => $customerService->searchCustomers($searchTerm), 'search_customers', 'Search for customers by name, email, city, or county')
->addTool(fn(int $customerId) => $customerService->getCustomerDetails($customerId), 'get_customer_details', 'Get detailed information about a specific customer including order history')
->addTool(fn(int $limit = 20) => $customerService->getRecentOrders($limit), 'get_recent_orders', 'Get a list of recent orders with customer information')
->addTool(fn(string $county) => $customerService->getCustomersByCounty($county), 'get_customers_by_county', 'Find all customers in a specific UK county')
->addTool(fn(int $limit = 10) => $customerService->getTopSpenders($limit), 'get_top_spenders', 'Get the top spending customers')
->build();
// Run the server with STDIO transport
$transport = new StdioTransport();
$server->run($transport);
This approach manually registers each tool method using closures. This ensures that Symfony's dependency injection properly provides the required repository dependencies to the CustomerMcpService.
In order to work, this file needs to be executable, so run the following command to do so:
chmod +x bin/mcp-server.php
Run the following command to test that the server starts:
php bin/mcp-server.php
The server will appear to "hang" with no output - this is correct behavior. MCP servers communicate via stdio (standard input/output) and wait silently for JSON-RPC messages from an MCP client. You can press Ctrl+C to stop the server.
Note: You won't see any prompts or logs when running the server directly. The server only responds to MCP protocol messages sent by clients like Claude Desktop.
Configure Claude Desktop
Locate Claude Desktop Config
The config file location depends on your OS:
-
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json -
Windows:
%APPDATA%\Claude\claude_desktop_config.json -
Linux:
~/.config/Claude/claude_desktop_config.json
If the file or directory doesn't exist, create it:
# macOS
mkdir -p ~/Library/Application\ Support/Claude
touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
# Linux
mkdir -p ~/.config/Claude
touch ~/.config/Claude/claude_desktop_config.json
You can open the file in your default editor:
# macOS - use full path to avoid permission issues
open -e "/Users/$(whoami)/Library/Application Support/Claude/claude_desktop_config.json"
# Linux
xdg-open ~/.config/Claude/claude_desktop_config.json
Add Your MCP Server
Edit the config file and add your server:
{
"mcpServers": {
"symfony-customers": {
"command": "php",
"args": [
"/absolute/path/to/your/app/bin/mcp-server.php"
],
"cwd": "/absolute/path/to/your/app"
}
}
}
Replace /absolute/path/to/your/app with the actual path to your application. For example, if your app is at /Users/yourname/projects/symfony-app, the config would be:
{
"mcpServers": {
"symfony-customers": {
"command": "php",
"args": [
"/Users/yourname/projects/symfony-app/bin/mcp-server.php"
],
"cwd": "/Users/yourname/projects/symfony-app"
}
}
}
Important Notes:
- The
cwd(current working directory) is required so Symfony can find its dependencies, configuration, and database - Both paths must be absolute (not relative)
- Use the full path to
bin/mcp-server.phpin theargsarray
Restart Claude Desktop
Completely quit and restart Claude Desktop for the changes to take effect.
Test with Claude
Once configured, you can ask Claude questions like:
Search for customers:
"Search for customers in London"
Get customer details:
"Show me details for customer ID 5"
Analyse orders:
"What are the recent orders?"
Find top spenders:
"Who are the top 5 spending customers?"
County-based queries:
"Show me all customers in Greater Manchester"
Claude will use the MCP tools you created to query your database and provide answers!
Add More Tools
You can extend the MCP server with additional tools:
Example: Product Search Tool
#[McpTool(
name: 'search_products',
description: 'Search for products by name or category'
)]
public function searchProducts(string $query): array
{
// Your implementation
}
Example: Order Statistics Tool
#[McpTool(
name: 'get_order_stats',
description: 'Get statistics about orders (total count, revenue, etc.)'
)]
public function getOrderStats(): array
{
// Your implementation
}
Next Steps
Here are some further steps I could think of, which I couldn't add to this tutorial but hope would further help your exploration of creating an MCP for your Symfony application:
- Add authentication to restrict MCP access
- Implement caching for frequently-queried data
- Add more sophisticated search capabilities
- Create resources in addition to tools
- Deploy the MCP server for production use


Top comments (0)