Ir al contenido principal

Symfony + MCP: cómo convertir tu API PHP en un servidor para agentes de IA

Guía práctica para exponer una API Symfony como servidor MCP: tools, resources, seguridad, permisos, transporte HTTP/STDIO y casos reales para agentes de IA.

Luis Miguel García Briz7 min de lectura
Compartir
Symfony + MCP: cómo convertir tu API PHP en un servidor para agentes de IA
Cargando audio...

Durante años una API era algo que consumía un frontend, una app móvil o una integración externa.

En 2026 la película cambia.

Tu API también puede ser consumida por agentes de IA.

Y eso abre una pregunta importante:

> ¿cómo expones acciones reales de tu sistema a un agente sin convertir tu backend en un buffet libre?

Aquí entra MCP, el Model Context Protocol.

MCP permite que un cliente de IA descubra herramientas, recursos y prompts disponibles en un servidor. Dicho fácil: es una forma estándar de decirle a un agente qué puede consultar, qué puede ejecutar y bajo qué contrato.

Lo interesante para PHP es que ya no hablamos solo de Node o Python.

Symfony ya tiene integración MCP mediante symfony/mcp-bundle, apoyada en el SDK oficial mcp/sdk.

Eso permite empezar a pensar en tu API Symfony como un servidor de tools para agentes.

No para darle acceso total.

Para darle acceso controlado.

De endpoint a tool#

Un endpoint tradicional está pensado para una interfaz concreta:

http
GET /api/posts?status=published

Un agente necesita una capacidad:

txt
listar_posts_publicados

Y esa capacidad debería tener:

  • nombre claro
  • descripción precisa
  • parámetros tipados
  • permisos
  • límites
  • salida controlada

No expones toda tu API.

Expones acciones concretas.

Instalación#

bash
composer require symfony/mcp-bundle

El bundle permite que Symfony actúe como servidor MCP usando transportes como HTTP o STDIO.

Para producción, yo empezaría siempre por un entorno privado o staging.

MCP no debería entrar directamente a producción sin autenticación, scopes, auditoría y rate limiting.

Primera tool MCP: listar posts públicos#

Imagina una web con blog técnico, servicios y herramientas públicas.

No quieres que el agente lea toda la base de datos.

Quieres que pueda consultar únicamente contenido publicado.

php
<?php

namespace App\Mcp\Tool;

use App\Repository\PostRepository;
use Symfony\McpBundle\Attribute\AsMcpTool;

#[AsMcpTool(
    name: 'list_public_posts',
    description: 'Lista los últimos artículos publicados del blog. Solo devuelve contenido público.'
)]
final readonly class ListPublicPostsTool
{
    public function __construct(
        private PostRepository $posts,
    ) {}

    public function __invoke(int $limit = 10): array
    {
        $limit = min(max($limit, 1), 20);

        return array_map(
            static fn ($post) => [
                'title' => $post->getTitle(),
                'slug' => $post->getSlug(),
                'excerpt' => $post->getExcerpt(),
                'publishedAt' => $post->getPublishedAt()?->format(DATE_ATOM),
            ],
            $this->posts->findPublished(limit: $limit)
        );
    }
}

Fíjate en varios detalles:

  • el nombre de la tool es semántico
  • la descripción dice cuándo usarla
  • el límite está acotado
  • solo se devuelven campos públicos
  • no se filtra nada sensible

Esto parece poca cosa, pero es justo lo que diferencia una integración segura de una integración peligrosa.

Tool de búsqueda controlada#

No hagas una tool genérica tipo query_database.

Eso es una mala idea.

Haz algo específico:

php
#[AsMcpTool(
    name: 'search_public_posts',
    description: 'Busca artículos públicos por texto. Úsala cuando el usuario pregunte por temas tratados en el blog.'
)]
final readonly class SearchPublicPostsTool
{
    public function __construct(
        private PostRepository $posts,
    ) {}

    public function __invoke(string $query, int $limit = 5): array
    {
        $query = trim($query);
        $limit = min(max($limit, 1), 10);

        if (mb_strlen($query) < 3) {
            return [
                'error' => 'La búsqueda debe tener al menos 3 caracteres.',
            ];
        }

        return $this->posts->searchPublished($query, $limit)
            ->map(static fn ($post) => [
                'title' => $post->getTitle(),
                'slug' => $post->getSlug(),
                'url' => sprintf('https://www.luismibriz.dev/blog/%s', $post->getSlug()),
                'excerpt' => $post->getExcerpt(),
            ])
            ->toArray();
    }
}

Esta tool ya permite a un agente contestar preguntas como:

> ¿Tienes algo escrito sobre MCP, Laravel o Next.js?

Pero sin darle acceso directo a tablas internas.

Lo que NO deberías exponer#

Un servidor MCP mal diseñado puede ser peor que una API mal documentada.

Yo evitaría exponer tools como estas:

txt
execute_sql
run_command
read_file
write_file
get_user_by_email
list_all_users
export_database
call_internal_endpoint

A veces tienen sentido en entornos internos muy controlados.

Pero en una web pública o un producto con datos reales, son peligrosas.

Mejor pensar en capabilities de negocio:

txt
search_public_posts
get_tool_documentation
create_contact_lead
get_service_summary
check_public_changelog

Más específico.

Más seguro.

Más fácil de auditar.

Seguridad mínima#

MCP no es solo otra integración.

Estás dando capacidades a agentes que pueden tomar decisiones, componer pasos y ejecutar herramientas.

Como mínimo necesitas:

  • autenticación del cliente MCP
  • autorización por tool
  • logs de cada llamada
  • rate limiting
  • validación fuerte de inputs
  • salidas sanitizadas
  • separación entre datos públicos y privados
  • entorno de pruebas antes de producción

Ejemplo simple de protección por bearer token:

php
final readonly class McpAuthMiddleware
{
    public function __construct(
        private string $expectedToken,
    ) {}

    public function __invoke(Request $request, callable $next): Response
    {
        $authorization = $request->headers->get('Authorization', '');
        $token = str_starts_with($authorization, 'Bearer ')
            ? substr($authorization, 7)
            : '';

        if (!hash_equals($this->expectedToken, (string) $token)) {
            throw new AccessDeniedHttpException('Invalid MCP token.');
        }

        return $next($request);
    }
}

Para producción real, esto debería evolucionar hacia tokens rotables, scopes, auditoría y permisos por herramienta.

Arquitectura hexagonal: MCP como adaptador#

MCP no debería entrar directamente hasta tu dominio.

Yo lo pondría como una capa de entrada más:

txt
MCP Client

MCP Tool Adapter

Application Use Case

Domain

Infrastructure

La tool no debería contener lógica de negocio compleja.

La tool debería llamar a un caso de uso:

php
#[AsMcpTool(
    name: 'get_service_summary',
    description: 'Devuelve un resumen público de los servicios profesionales ofrecidos.'
)]
final readonly class GetServiceSummaryTool
{
    public function __construct(
        private GetPublicServiceSummary $useCase,
    ) {}

    public function __invoke(): array
    {
        return ($this->useCase)();
    }
}

Así MCP es un adaptador.

No el centro de tu aplicación.

Cómo lo conectaría con Claude, Cursor o Copilot#

El cliente MCP dependerá de la herramienta que uses, pero el concepto es siempre parecido:

json
{
  "mcpServers": {
    "luismibriz-api": {
      "url": "https://api.luismibriz.dev/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${LUISMIBRIZ_MCP_TOKEN}"
      }
    }
  }
}

Y luego el agente puede pedir cosas como:

txt
Busca en el blog si hay contenido sobre pgvector, Laravel o OpenAPI.
Resume los 3 posts más relacionados.
No inventes URLs.

El agente ya no necesita adivinar.

Tiene una tool.

Demo en vivo

Conéctate a mi servidor MCP

Este blog no es solo teoría: el endpoint que describo más arriba está corriendo en producción. Añade este bloque al mcpServers de un cliente compatible con servidores remotos y headers bearer. Tu agente podrá hablar con la API de este portfolio en lectura, sin acceso al admin ni a datos sensibles.

json
{
  "mcpServers": {
    "luismibriz": {
      "url": "https://api.luismibriz.dev/api/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${LUISMIBRIZ_MCP_TOKEN}"
      }
    }
  }
}

Token: pídelo desde la página MCP. Lo emito manualmente desde Cholo y el plaintext se muestra una sola vez. No publico tokens embebidos en el JSON precisamente porque son auditables y eso me obliga a hacerlo bien.

Tools disponibles

  • list_public_posts

    Devuelve los últimos artículos publicados con título, slug, fecha, categoría y URL canónica.

    Prueba: “¿Qué has publicado últimamente en el blog?

  • search_public_posts

    Busca artículos por texto sobre título, excerpt y keywords. Los matches en título puntúan más alto.

    Prueba: “¿Tienes algo escrito sobre Symfony y MCP?

  • get_services_summary

    Lista los servicios profesionales activos con su resumen y URL pública.

    Prueba: “¿En qué servicios trabajas ahora mismo?

Errores comunes#

Exponer demasiado pronto#

Empieza por tools públicas y de solo lectura.

Luego ya pensarás en escritura.

Tools demasiado genéricas#

search no es tan útil como search_public_posts.

execute no es una tool. Es una bomba.

No auditar llamadas#

Cada ejecución MCP debería registrar:

  • tool llamada
  • usuario o cliente
  • parámetros
  • resultado
  • duración
  • errores

Devolver entidades completas#

Nunca devuelvas modelos enteros si no hace falta.

Devuelve DTOs pequeños y explícitos.

Checklist antes de producción#

  • [ ] Las tools tienen nombres claros
  • [ ] No existe ninguna tool genérica tipo execute_sql
  • [ ] Todas las entradas están validadas
  • [ ] Las salidas no filtran datos sensibles
  • [ ] Hay autenticación
  • [ ] Hay autorización por tool o scope
  • [ ] Hay logs de auditoría
  • [ ] Hay rate limiting
  • [ ] Hay entorno de staging
  • [ ] Las tools llaman a casos de uso
  • [ ] Los errores no exponen stack traces
  • [ ] Los secretos están fuera del repo

Conclusión#

Symfony + MCP es una combinación muy interesante porque une dos mundos que hasta hace poco parecían separados: backend PHP serio y agentes de IA.

Pero el enfoque correcto no es:

> voy a exponer mi API entera al agente.

El enfoque correcto es:

> voy a crear capabilities pequeñas, seguras, auditables y útiles.

Tu API ya no solo la consumen frontends.

También pueden consumirla agentes.

Y precisamente por eso hay que diseñarla mejor, no peor.

Fuentes#

  • Symfony MCP Bundle: https://symfony.com/doc/current/ai/bundles/mcp-bundle.html
  • Symfony MCP Package: https://symfony.com/packages/mcp-bundle
  • MCP PHP SDK oficial: https://github.com/modelcontextprotocol/php-sdk

Avisos sin ruido

Nuevas herramientas, cuando salgan.

Deja tu email y te aviso cuando publique calculadoras, herramientas o aprendizajes técnicos relevantes.

Este aviso llega a [email protected] para gestionarlo manualmente. Sin spam ni listas raras. Puedes darte de baja cuando quieras desde desuscribirte.