<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Edouard Courty</title>
    <description>The latest articles on DEV Community by Edouard Courty (@edouardcourty).</description>
    <link>https://dev.to/edouardcourty</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3270226%2Fcc412239-3f5f-4c8a-95de-48d50385c1eb.jpg</url>
      <title>DEV Community: Edouard Courty</title>
      <link>https://dev.to/edouardcourty</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edouardcourty"/>
    <language>en</language>
    <item>
      <title>How to create an MCP server with Symfony</title>
      <dc:creator>Edouard Courty</dc:creator>
      <pubDate>Tue, 17 Jun 2025 09:41:11 +0000</pubDate>
      <link>https://dev.to/edouardcourty/how-to-create-an-mcp-server-with-symfony-3bf7</link>
      <guid>https://dev.to/edouardcourty/how-to-create-an-mcp-server-with-symfony-3bf7</guid>
      <description>&lt;p&gt;Model Context Protocol (MCP) is the new big thing in the AI revolution.&lt;/p&gt;

&lt;p&gt;Large Language Models (LLMs) are powerful, but &lt;strong&gt;they do not have access to the outside world&lt;/strong&gt;: they cannot navigate on the Internet, browse their host machine’s file system, access an external database…&lt;/p&gt;

&lt;p&gt;However, these LLMs can be enhanced with capacities, which are called tools.&lt;/p&gt;

&lt;p&gt;These tools are not built into the LLM. They’re provided by you, the developer.&lt;br&gt;
The way it works is the Agent (An app using an LLM to function) is given, in addition to the prompt, a list of external tools which can be used to fulfill the user’s need.&lt;/p&gt;

&lt;p&gt;The LLM then decides if it’s able to fulfill the request on its own (like craft a piece of text) or if it needs to &lt;strong&gt;use a tool that matches its needs&lt;/strong&gt; in the provided list.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol defines a way of communicating between what could be called “agents” (Clients) and external apps (Servers).&lt;/p&gt;

&lt;p&gt;The official MCP specification states MCP servers should handle HTTP communication using &lt;strong&gt;JSON-RPC&lt;/strong&gt;.&lt;br&gt;
These 3 methods should be handled by any server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize&lt;/code&gt; — Allows the version and capabilities exchange between the client and server,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/list&lt;/code&gt; — Returns the server’s available tools, each containing a title, description, and input format,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tools/call&lt;/code&gt; — Executes a tool given its name and arguments.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The MCP Server Bundle
&lt;/h2&gt;

&lt;p&gt;Creating an MCP server with Symfony just became very, very easy.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/EdouardCourty/mcp-server-bundle" rel="noopener noreferrer"&gt;MCP Server Bundle&lt;/a&gt; is a plug-and-play Symfony bundle that does all the heavy-lifting for you. It handles the transport and presentation layer, allowing you to focus solely on building useful tools.&lt;/p&gt;

&lt;p&gt;It handles the JSON-RPC methods the MCP specification provides (initialization, tool listing and tool calling), using the tools you built within your project.&lt;/p&gt;
&lt;h3&gt;
  
  
  🧰 Getting started
&lt;/h3&gt;

&lt;p&gt;To start building an MCP server, install the MCP Server Bundle in your Symfony project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require ecourty/mcp-server-bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;After this, if not using Symfony Flex, add the bundle to your bundles.php file.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mf"&gt;...&lt;/span&gt;
    &lt;span class="nc"&gt;Ecourty\McpServerBundle\McpServerBundle&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set up the routing by creating a &lt;code&gt;mcp.yaml&lt;/code&gt; file to your &lt;code&gt;config/routes&lt;/code&gt; directory like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mcp_controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mcp&lt;/span&gt;
  &lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp_server.entrypoint_controller&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🛠️ Le’s build a tool!
&lt;/h3&gt;

&lt;p&gt;Building your first tool is pretty straightforward.&lt;/p&gt;

&lt;p&gt;The bundle provides an &lt;code&gt;AsTool&lt;/code&gt; PHP attribute, which you can add to a class to make it available as a tool within the MCP server.&lt;br&gt;
The attribute is used to define the tool name, description, and some optional flags called annotations.&lt;/p&gt;

&lt;p&gt;This attribute will make the class become a registered tool, given the following conditions are met:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The class has the &lt;code&gt;AsTool&lt;/code&gt; attribute&lt;/li&gt;
&lt;li&gt;The class has an &lt;code&gt;__invoke&lt;/code&gt; method which returns an instance of ToolResult&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;__invoke&lt;/code&gt; method should have only one parameter, typed with a class which public properties contain OpenAPI attributes and optionally assertions (which will be enforced)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Let’s build a tool that retrieves the content of a URL!&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Let’s build the input schema first:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;OpenApi\Attributes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="no"&gt;OA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetrieveURLContent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Assert\Url]&lt;/span&gt;
    &lt;span class="na"&gt;#[OA\Property(type: 'string', format: 'uri', nullable: false)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Now, let’s build the actual tool!&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Tool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Schema\RetrieveURLContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Ecourty\McpServerBundle\Attribute\AsTool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Ecourty\McpServerBundle\Attribute\ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Ecourty\McpServerBundle\IO\TextToolResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Ecourty\McpServerBundle\IO\ToolResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AsTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'get_url_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Retrieves the content of a URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolAnnotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// Optional, provides more information about the tool's behavior&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Retrieve URL Content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;readOnlyHint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;destructiveHint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;idempotentHint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;openWorldHint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RetrieveURLContentTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RetrieveURLContent&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ToolResult&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextToolResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'The URL did not return anything'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;isError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolResult&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextToolResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You’re done! 🥳&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your tool should be registered in your Symfony app and be available for any MCP client.&lt;/p&gt;

&lt;h3&gt;
  
  
  📡 Testing your MCP Server
&lt;/h3&gt;

&lt;p&gt;You can now use the debug command to see if your tool was created properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/console debug:mcp-tools

MCP Tools Debug Information
&lt;span class="o"&gt;===========================&lt;/span&gt;

&lt;span class="nt"&gt;------------------&lt;/span&gt; &lt;span class="nt"&gt;---------------------------------&lt;/span&gt; &lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
 Name               Description                       Input Schema                                           
&lt;span class="nt"&gt;------------------&lt;/span&gt; &lt;span class="nt"&gt;---------------------------------&lt;/span&gt; &lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
 get_url_content    Retrieves the content of a URL    App&lt;span class="se"&gt;\S&lt;/span&gt;chema&lt;span class="se"&gt;\R&lt;/span&gt;etrieveURLContent     
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect, now your tool is ready to be used by MCP clients!&lt;/p&gt;

&lt;p&gt;Try to call &lt;code&gt;http://localhost:&amp;lt;port&amp;gt;/mcp&lt;/code&gt; with the &lt;code&gt;tools/list&lt;/code&gt; JSON-RPC method, and you should receive a JSON response listing your available tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST http://localhost:8080/mcp
Request
{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
}

Response
{
    "jsonrpc": "2.0",
    "id": "list",
    "result": {
        "tools": [
            {
                "name": "get_url_content",
                "description": "Retrieves the content of a URL",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": false
                        }
                    }
                },
                "annotations": {
                    "title": "Retrieve URL Content",
                    "readOnlyHint": true,
                    "destructiveHint": false,
                    "idempotentHint": false,
                    "openWorldHint": true
                }
            }
        ]
    },
    "error": null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Going Further
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/EdouardCourty/mcp-server-bundle" rel="noopener noreferrer"&gt;MCP Server Bundle&lt;/a&gt; provides cool features to help you build MCP servers, such as the ability to listen to events, write custom JSON-RPC method handing logic, or create custom tool results.&lt;/p&gt;

&lt;p&gt;If you want to know more about how this works under the hood, feel free to visit the repo, and why not make a contribution if you want to participate in the project!&lt;/p&gt;

&lt;h3&gt;
  
  
  Using MCP servers in practice
&lt;/h3&gt;

&lt;p&gt;Alright. You’ve built your server, you created a few tools, but what next?&lt;/p&gt;

&lt;p&gt;You need to give access to your MCP server to your client.&lt;br&gt;
Check the documentation for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual Studio Code:
&lt;a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JetBrains IDE:
&lt;a href="https://www.jetbrains.com/help/ai-assistant/configure-an-mcp-server.html" rel="noopener noreferrer"&gt;https://www.jetbrains.com/help/ai-assistant/configure-an-mcp-server.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;n8n :
&lt;a href="https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp/" rel="noopener noreferrer"&gt;https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolmcp/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>php</category>
      <category>symfony</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
