Skip to content

Conversation

@camilleislasse
Copy link
Contributor

Q A
Bug fix? no
New feature? yes
Docs? no
License MIT

This PR adds a proof-of-concept MCP client implementation allowing Symfony AI agents to connect to remote MCP servers and use their tools.

⚠️ This is a POC - The implementation is functional but not optimized for production use. It serves as a testing ground while waiting for the official PHP SDK to implement its client component (currently on their roadmap). Once available, this implementation should be migrated to use it.

How it works

1. Transports

Three transport implementations for different MCP server types:

Transport Use case
SseTransport Gradio/HuggingFace Spaces (Server-Sent Events)
HttpTransport Streamable HTTP
StdioTransport Local process via stdin/stdout

2. McpToolbox

Adapts MCP tools to Symfony AI's ToolboxInterface:

  • Converts MCP tool definitions to Tool objects
  • Executes tools via McpClient::callTool()
  • Converts MCP content types to Platform types:
MCP Type Platform Type
TextContent Text
ImageContent Image (base64 decoded)
AudioContent Audio (base64 decoded)
EmbeddedResource Text / Image / Audio / File

3. ChainToolbox

Combines multiple ToolboxInterface into one, enabling agents to use tools from different sources (local + multiple MCPs).

4. ToolCallMessage changes

ToolCallMessage now supports multiple content types (not just string):

// Before
new ToolCallMessage($toolCall, 'text result');

// After
new ToolCallMessage($toolCall, new Text('...'), new Image($data, 'image/png'));

This enables MCP tools to return images/audio that get displayed in chat.

5. Bundle Integration

ai:
    mcp:
        my_mcp:
            transport: sse  # or http, stdio
            url: 'https://example.com/mcp/sse'
            tools:
                - 'tool_name'  # Optional: filter exposed tools

Creates services:

  • ai.mcp.client.{name} - The MCP client
  • ai.mcp.toolbox.{name} - The toolbox (use this in agent config)

Use in agent:

ai:
    agent:
        my_agent:
            tools:
                - 'ai.mcp.toolbox.my_mcp'

Breaking Changes

This PR introduces breaking changes to enable multimodal tool results:

1. ToolCallMessage::getContent() return type changed

// Before
public function getContent(): string

// After
public function getContent(): array // ContentInterface[]

Migration: Use $message->asText() to get the text content as a string.

2. ToolResultConverter::convert() return type changed

// Before
public function convert(ToolResult $toolResult): ?string

// After
public function convert(ToolResult $toolResult): array // ContentInterface[]

3. ToolCallMessage constructor signature changed

// Before
new ToolCallMessage($toolCall, 'content string')

// After (variadic)
new ToolCallMessage($toolCall, 'string', $image, $audio, ...)
new ToolCallMessage($toolCall, new Text('...'), new Image(...))

Passing a single string still works but internally converts to Text content.


Demo

Timeline bot combining two MCP servers:

  1. Live City MCP → fetches news for a city
  2. Graphify → generates a timeline diagram from the news

User asks about a city → Agent fetches news → Agent generates timeline → Image displayed in chat.

ai:
    mcp:
        graphify:
            transport: sse
            url: 'https://agents-mcp-hackathon-graphify.hf.space/gradio_api/mcp/sse'
            tools:
                - 'Graphify_generate_timeline_diagram'
        city:
            transport: sse
            url: 'https://kingabzpro-live-city-mcp.hf.space/gradio_api/mcp/sse'
            tools:
                - 'live_city_mcp_get_city_news'
    agent:
        timeline:
            platform: 'ai.platform.openai'
            model: 'gpt-4o-mini'
            prompt: |
                You are a news timeline generator. When the user asks about a city:
                1) First use live_city_mcp_get_city_news to fetch news for that city
                2) Then use Graphify_generate_timeline_diagram with this JSON format:
                {"title": "News from [City]", "events_per_row": 3, "events": [{"id": "1", "label": "Short title", "date": "2024-12-13"}]}
            tools:
                - 'ai.mcp.toolbox.graphify'
                - 'ai.mcp.toolbox.city'
Capture d’écran 2025-12-13 à 10 29 22

@carsonbot carsonbot added Status: Needs Review Agent Issues & PRs about the AI Agent component Feature New feature labels Dec 13, 2025
@camilleislasse camilleislasse force-pushed the feature/mcp-client branch 2 times, most recently from f533f60 to 5e4c5ed Compare December 13, 2025 10:38
@camilleislasse camilleislasse marked this pull request as draft December 13, 2025 20:02
@chr-hertel
Copy link
Member

First of all, thanks @camilleislasse, this is awesome - really looking forward to merging this!
This might take some thoughts and iterations, but your PR is a great start for that!
That toolbox problem is not easy to wrap my head around ...

To move on, I'd like to approach this from two sides:

  • Where do we want to go in the end?
  • How can we split this up into smaller steps?

Where do we want to go in the end?
This is a bit tricky since we have to imagine how the mcp client implementation will come to life - but I would see it in combination with the MCP bundle. The Client would rather be configured using that bundle, and an MCP tool(box) would rather reference that as client (incl. transport) - we can skip that for now, I'm just bringing that up because i feel like the ai.mcp level on bundle config will be deprecated than.

I think, at least one day, we'd rather focusing on an service to configure or referencing the mcp client directly:
going with a service:

ai:
    agent:
        timeline:
            platform: 'ai.platform.openai'
            model: 'gpt-4o-mini'
            prompt: # ...
            tools:
                - 'mcp.tool.graphify'

service:
    mcp.tool.graphify:
        class: Symfony\AI\Agent\Bridge\Mcp\Tool
        arguments:
             $client: 'mcp.client.graphify' # transport etc. is already configured with that service
             $tools: [...] # optional list to filter tools
             $resources: [...] # optional list to filter resources
             $prompts: [...] # optional list to filter prompts

An alternative could be to go a similar way how we deal with subagents or services - inlining it:

ai:
    agent:
        timeline:
            platform: 'ai.platform.openai'
            model: 'gpt-4o-mini'
            prompt: # ...
            tools:
                - mcp: 'mcp.client.graphify'

maybe with allowing those filters on the client config already - similar to what you have on clients like Claude Desktop.

How can we split this up into smaller steps?
The beauty of a mono repo is that we can see all changes in one PR, the downside is large PRs :D so let's go a bit incremental here.

Let's keep this as a working draft and pull out parts of it as standalone steps, that we review & merge independently, e.g.

  1. Platform Changes
  2. ChainToolbox
  3. MCP Bridge + Bundle
  4. Demo
  5. Docs

(just as example)

And with that making this PR smaller step by step

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Issues & PRs about the AI Agent component Feature New feature Status: Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants