|
| 1 | +## [Agent] Enable remote MCP tools for agents |
| 2 | + |
| 3 | +This PR adds a **proof-of-concept** MCP client implementation allowing Symfony AI agents to connect to remote MCP servers and use their tools. |
| 4 | + |
| 5 | +> ⚠️ **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](https://github.com/modelcontextprotocol/php-sdk) to implement its client component (currently on their roadmap). Once available, this implementation should be migrated to use it. |
| 6 | +
|
| 7 | + |
| 8 | +### How it works |
| 9 | + |
| 10 | +#### 1. Transports |
| 11 | + |
| 12 | +Three transport implementations for different MCP server types: |
| 13 | + |
| 14 | +| Transport | Use case | |
| 15 | +|-----------|----------| |
| 16 | +| `SseTransport` | Gradio/HuggingFace Spaces (Server-Sent Events) | |
| 17 | +| `HttpTransport` | Streamable HTTP | |
| 18 | +| `StdioTransport` | Local process via stdin/stdout | |
| 19 | + |
| 20 | +#### 2. McpToolbox |
| 21 | + |
| 22 | +Adapts MCP tools to Symfony AI's `ToolboxInterface`: |
| 23 | +- Converts MCP tool definitions to `Tool` objects |
| 24 | +- Executes tools via `McpClient::callTool()` |
| 25 | +- Converts MCP content types to Platform types: |
| 26 | + |
| 27 | +| MCP Type | Platform Type | |
| 28 | +|----------|---------------| |
| 29 | +| `TextContent` | `Text` | |
| 30 | +| `ImageContent` | `Image` (base64 decoded) | |
| 31 | +| `AudioContent` | `Audio` (base64 decoded) | |
| 32 | +| `EmbeddedResource` | `Text` / `Image` / `Audio` / `File` | |
| 33 | + |
| 34 | +#### 3. ChainToolbox |
| 35 | + |
| 36 | +Combines multiple `ToolboxInterface` into one, enabling agents to use tools from different sources (local + multiple MCPs). |
| 37 | + |
| 38 | +#### 4. ToolCallMessage changes |
| 39 | + |
| 40 | +`ToolCallMessage` now supports multiple content types (not just string): |
| 41 | +```php |
| 42 | +// Before |
| 43 | +new ToolCallMessage($toolCall, 'text result'); |
| 44 | + |
| 45 | +// After |
| 46 | +new ToolCallMessage($toolCall, new Text('...'), new Image($data, 'image/png')); |
| 47 | +``` |
| 48 | + |
| 49 | +This enables MCP tools to return images/audio that get displayed in chat. |
| 50 | + |
| 51 | +#### 5. Bundle Integration |
| 52 | + |
| 53 | +```yaml |
| 54 | +ai: |
| 55 | + mcp: |
| 56 | + my_mcp: |
| 57 | + transport: sse # or http, stdio |
| 58 | + url: 'https://example.com/mcp/sse' |
| 59 | + tools: |
| 60 | + - 'tool_name' # Optional: filter exposed tools |
| 61 | +``` |
| 62 | +
|
| 63 | +Creates services: |
| 64 | +- `ai.mcp.client.{name}` - The MCP client |
| 65 | +- `ai.mcp.toolbox.{name}` - The toolbox (use this in agent config) |
| 66 | + |
| 67 | +Use in agent: |
| 68 | +```yaml |
| 69 | +ai: |
| 70 | + agent: |
| 71 | + my_agent: |
| 72 | + tools: |
| 73 | + - 'ai.mcp.toolbox.my_mcp' |
| 74 | +``` |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +### Breaking Changes |
| 79 | + |
| 80 | +This PR introduces breaking changes to enable multimodal tool results: |
| 81 | + |
| 82 | +#### 1. `ToolCallMessage::getContent()` return type changed |
| 83 | + |
| 84 | +```php |
| 85 | +// Before |
| 86 | +public function getContent(): string |
| 87 | +
|
| 88 | +// After |
| 89 | +public function getContent(): array // ContentInterface[] |
| 90 | +``` |
| 91 | + |
| 92 | +**Migration**: Use `$message->asText()` to get the text content as a string. |
| 93 | + |
| 94 | +#### 2. `ToolResultConverter::convert()` return type changed |
| 95 | + |
| 96 | +```php |
| 97 | +// Before |
| 98 | +public function convert(ToolResult $toolResult): ?string |
| 99 | +
|
| 100 | +// After |
| 101 | +public function convert(ToolResult $toolResult): array // ContentInterface[] |
| 102 | +``` |
| 103 | + |
| 104 | +#### 3. `ToolCallMessage` constructor signature changed |
| 105 | + |
| 106 | +```php |
| 107 | +// Before |
| 108 | +new ToolCallMessage($toolCall, 'content string') |
| 109 | +
|
| 110 | +// After (variadic) |
| 111 | +new ToolCallMessage($toolCall, 'string', $image, $audio, ...) |
| 112 | +new ToolCallMessage($toolCall, new Text('...'), new Image(...)) |
| 113 | +``` |
| 114 | + |
| 115 | +Passing a single string still works but internally converts to `Text` content. |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +### Demo |
| 120 | + |
| 121 | +`Timeline` bot combining two MCP servers: |
| 122 | +1. **Live City MCP** → fetches news for a city |
| 123 | +2. **Graphify** → generates a timeline diagram from the news |
| 124 | + |
| 125 | +User asks about a city → Agent fetches news → Agent generates timeline → Image displayed in chat. |
| 126 | + |
| 127 | +```yaml |
| 128 | +ai: |
| 129 | + mcp: |
| 130 | + graphify: |
| 131 | + transport: sse |
| 132 | + url: 'https://agents-mcp-hackathon-graphify.hf.space/gradio_api/mcp/sse' |
| 133 | + tools: |
| 134 | + - 'Graphify_generate_timeline_diagram' |
| 135 | + city: |
| 136 | + transport: sse |
| 137 | + url: 'https://kingabzpro-live-city-mcp.hf.space/gradio_api/mcp/sse' |
| 138 | + tools: |
| 139 | + - 'live_city_mcp_get_city_news' |
| 140 | + agent: |
| 141 | + timeline: |
| 142 | + platform: 'ai.platform.openai' |
| 143 | + model: 'gpt-4o-mini' |
| 144 | + prompt: | |
| 145 | + You are a news timeline generator. When the user asks about a city: |
| 146 | + 1) First use live_city_mcp_get_city_news to fetch news for that city |
| 147 | + 2) Then use Graphify_generate_timeline_diagram with this JSON format: |
| 148 | + {"title": "News from [City]", "events_per_row": 3, "events": [{"id": "1", "label": "Short title", "date": "2024-12-13"}]} |
| 149 | + tools: |
| 150 | + - 'ai.mcp.toolbox.graphify' |
| 151 | + - 'ai.mcp.toolbox.city' |
| 152 | +``` |
0 commit comments