DEV Community

Cover image for Build a Model Context Protocol (MCP) Server for Symfony
Greg Holmes
Greg Holmes

Posted on • Originally published at gregholmes.dev

Build a Model Context Protocol (MCP) Server for Symfony

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install the dependencies with the single composer install command:

composer install
Enter fullscreen mode Exit fullscreen mode

Let's make sure the starter branch is working as expected by starting up the server:

php -S localhost:8000 -t public
Enter fullscreen mode Exit fullscreen mode

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:

Screenshot of the Customer Management System's Customers page displaying a data table with 100 customer records. The table shows customer details including ID, Name, Email, City, County, number of Orders, Total Spent, and an Actions column with View buttons. The page features a dark navigation header with links to Customers, Orders, and Products sections. Alt text: Customer Management System Customers page showing a table of 6 visible customers with details like names, emails, locations, order counts, and total spending amounts.

While the second shows a table of 100 order records by customers:

Screenshot of the Customer Management System's Recent Orders page showing a table of 100 order records. The table displays Order ID, Customer (with clickable links), Date, Status (with color-coded badges for CANCELLED, DELIVERED, PROCESSING, and SHIPPED), Items count, and Total amounts. The interface uses the same dark navigation header as the Customers page. Alt text: Customer Management System Recent Orders page displaying order records with color-coded status badges including cancelled, delivered, processing, and shipped orders from October 2025.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Important: The App\Service\CustomerMcpService declaration must come after the App\: 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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run the following command to test that the server starts:

php bin/mcp-server.php
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.php in the args array

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
}
Enter fullscreen mode Exit fullscreen mode

Example: Order Statistics Tool

#[McpTool(
    name: 'get_order_stats',
    description: 'Get statistics about orders (total count, revenue, etc.)'
)]
public function getOrderStats(): array
{
    // Your implementation
}
Enter fullscreen mode Exit fullscreen mode

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

Resources

Top comments (0)