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:
GET /api/posts?status=publishedUn agente necesita una capacidad:
listar_posts_publicadosY 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#
composer require symfony/mcp-bundleEl 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
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:
#[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:
execute_sql
run_command
read_file
write_file
get_user_by_email
list_all_users
export_database
call_internal_endpointA 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:
search_public_posts
get_tool_documentation
create_contact_lead
get_service_summary
check_public_changelogMá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:
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:
MCP Client
↓
MCP Tool Adapter
↓
Application Use Case
↓
Domain
↓
InfrastructureLa tool no debería contener lógica de negocio compleja.
La tool debería llamar a un caso de uso:
#[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:
{
"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:
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.
{
"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
Más sobre PHP
Ver todos →