{"id":"ec1d38bb-06b5-4823-9c8b-b1061ac1b7bb","headline":"symfony-command-ui: tus comandos Symfony son tu MCP server","slug":"symfony-command-ui-tus-comandos-symfony-son-tu-mcp-server","articleBody":"<h1 id=\"el-problema-que-todos-tenemos\">El problema que todos\ntenemos<\/h1>\n<p>Si trabajas con Symfony, tienes comandos de consola. Muchos.\n<code>app:users:sync<\/code>, <code>app:payments:process<\/code>,\n<code>app:cache:warmup<\/code>\u2026 Son la columna vertebral de tu\naplicacion: encapsulan logica de negocio, se ejecutan en cron, los usas\npara debuggear, para migraciones, para todo.<\/p>\n<p>Y sin embargo, ejecutarlos es un rollo:<\/p>\n<ul>\n<li>SSH al servidor (o al pod de Kubernetes)<\/li>\n<li>Recordar la sintaxis exacta (<code>--limit=100<\/code> o\n<code>--limit 100<\/code>?)<\/li>\n<li>Copiar el output a mano si quieres compartirlo<\/li>\n<li>Si estas en un pod de K8s, ni siquiera tienes terminal a veces<\/li>\n<\/ul>\n<p>Y ahora, en la era agentica, hay un segundo problema: <strong>como le\ndices a un agente de IA que ejecute tus comandos?<\/strong> Los LLMs son\nbuenos entendiendo JSON, haciendo HTTP, procesando streams. Pero no\npueden hacer SSH a tu pod.<\/p>\n<h1 id=\"la-solucion-un-bundle-que-los-expone-a-todos\">La solucion: un\nbundle que los expone a todos<\/h1>\n<p><code>symfony-command-ui<\/code> es un bundle de Symfony que he\npublicado hoy. Hace una cosa simple:<\/p>\n<p><strong>Convierte tus comandos de consola en una UI web con terminal\nstreaming Y en una API JSON que cualquier agente de IA puede\nusar.<\/strong><\/p>\n<div class=\"sourceCode\" id=\"cb1\"><pre\nclass=\"sourceCode bash\"><code class=\"sourceCode bash\"><span id=\"cb1-1\"><a href=\"#cb1-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"ex\">composer<\/span> require pascualmg\/symfony-command-ui<\/span><\/code><\/pre><\/div>\n<p>Tres lineas de YAML y tienes un dashboard con todos tus comandos,\nauto-descubiertos, con formularios generados automaticamente desde tu\n<code>InputDefinition<\/code>.<\/p>\n<h1 id=\"como-funciona\">Como funciona<\/h1>\n<p>La arquitectura es simple:<\/p>\n<ol>\n<li>El bundle ejecuta <code>php bin\/console list --format=json<\/code>\npara descubrir los comandos disponibles<\/li>\n<li>Filtra por una whitelist que tu configuras (seguridad)<\/li>\n<li>Traduce cada <code>InputOption<\/code> y <code>InputArgument<\/code> a\nun formulario web (checkboxes, dropdowns, text inputs)<\/li>\n<li>Cuando pulsas Run, ejecuta el comando via\n<code>Symfony\\Component\\Process<\/code> y te streama el output en tiempo\nreal como NDJSON<\/li>\n<\/ol>\n<p>Cada comando se renderiza como una <strong>card\nindependiente<\/strong> con su propio formulario y su propia terminal.\nLos outputs persisten: puedes ejecutar Stats mientras Generate JWT\nmantiene su resultado.<\/p>\n<h1 id=\"la-parte-interesante-interfaz-dual\">La parte interesante:\ninterfaz dual<\/h1>\n<p>Aqui es donde se pone bueno. El bundle expone <strong>dos interfaces\ncon los mismos endpoints<\/strong>:<\/p>\n<h2 id=\"para-humanos-el-dashboard-web\">Para humanos: el dashboard\nweb<\/h2>\n<p>Un Web Component (<code>&lt;symfony-command&gt;<\/code>) con Shadow\nDOM, zero dependencias, que descubre los comandos al montar y renderiza\ntodo automaticamente. No hay npm, no hay webpack, no hay build step. Un\nunico fichero JS que el bundle sirve como asset.<\/p>\n<p>CSS customizable via custom properties. Dark theme por defecto.\nTheming completo si quieres.<\/p>\n<h2 id=\"para-agentes-la-api-json\">Para agentes: la API JSON<\/h2>\n<p>Dos endpoints:<\/p>\n<ul>\n<li><code>GET \/commands<\/code>: devuelve un JSON estructurado con todos\nlos comandos disponibles, sus opciones, defaults y tipos<\/li>\n<li><code>POST \/execute<\/code>: recibe <code>{command, options}<\/code> y\nstreama el output como NDJSON<\/li>\n<\/ul>\n<p>Cualquier agente que hable HTTP puede:<\/p>\n<ol>\n<li>Descubrir que operaciones existen en tu app<\/li>\n<li>Entender sus parametros (sin documentacion extra \u2013 salen del\n<code>InputDefinition<\/code> de Symfony)<\/li>\n<li>Ejecutarlas y observar el resultado en tiempo real<\/li>\n<\/ol>\n<p>Es un <strong>MCP server gratis<\/strong>. Sin instalar nada especial.\nSin protocolo custom. Solo HTTP + JSON.<\/p>\n<h1 id=\"un-ejemplo-real-claude-operando-una-app\">Un ejemplo real: Claude\noperando una app<\/h1>\n<p>Imagina que tienes un e-commerce con estos comandos:<\/p>\n<div class=\"sourceCode\" id=\"cb2\"><pre\nclass=\"sourceCode yaml\"><code class=\"sourceCode yaml\"><span id=\"cb2-1\"><a href=\"#cb2-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"fu\">allowed_commands<\/span><span class=\"kw\">:<\/span><\/span>\n<span id=\"cb2-2\"><a href=\"#cb2-2\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"at\">    <\/span><span class=\"kw\">-<\/span><span class=\"at\"> app:orders:pending<\/span><\/span>\n<span id=\"cb2-3\"><a href=\"#cb2-3\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"at\">    <\/span><span class=\"kw\">-<\/span><span class=\"at\"> app:orders:process<\/span><\/span>\n<span id=\"cb2-4\"><a href=\"#cb2-4\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"at\">    <\/span><span class=\"kw\">-<\/span><span class=\"at\"> app:inventory:check<\/span><\/span>\n<span id=\"cb2-5\"><a href=\"#cb2-5\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"at\">    <\/span><span class=\"kw\">-<\/span><span class=\"at\"> app:reports:daily<\/span><\/span><\/code><\/pre><\/div>\n<p>Un agente de IA (Claude, GPT, tu propio agente) puede hacer:<\/p>\n<blockquote>\n<p><strong>Tu<\/strong>: \"Hay pedidos pendientes de mas de 500\neuros?\"<\/p>\n<p><strong>Agente<\/strong>: \/llama GET <em>commands, encuentra\napp:orders:pending<\/em> \/llama POST <em>execute con\n{\"command\":\"app:orders:pending\",\"options\":{\"\u2013min-amount\":500,\"\u2013json\":true}}<\/em><\/p>\n<p>\"Si, hay 3 pedidos pendientes por encima de 500 euros: #4521 (720\neuros), #4523 (1100 euros), #4529 (550 euros). Quieres que los\nprocese?\"<\/p>\n<p><strong>Tu<\/strong>: \"Primero con dry-run\"<\/p>\n<p><strong>Agente<\/strong>: \/llama POST <em>execute con\n{\"command\":\"app:orders:process\",\"options\":{\"\u2013ids\":\"4521,4523,4529\",\"\u2013dry-run\":true}}<\/em><\/p>\n<p>\"Dry run completado. Los 3 se procesarian correctamente. Total: 2370\neuros. Ejecuto en real?\"<\/p>\n<\/blockquote>\n<p>Todo esto funciona porque tus comandos de Symfony <strong>ya\nencapsulan la logica<\/strong>. El bundle solo la expone.<\/p>\n<h1 id=\"el-patron-logica-en-comandos-interfaz-como-adaptador\">El patron:\nlogica en comandos, interfaz como adaptador<\/h1>\n<p>Si haces DDD o hexagonal, tus comandos de Symfony son\n<strong>adaptadores CLI<\/strong> que llaman a use cases del dominio.\nEste bundle anade un segundo adaptador (HTTP) al mismo use case, sin\ntocar una linea de tu logica de negocio.<\/p>\n<pre><code>                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n      bin\/console\u2500\u2500\u25ba\u2502              \u2502\n                    \u2502  Use Case    \u2502\nsymfony-command-ui\u2500\u2500\u25ba\u2502  (dominio)   \u2502\n      (HTTP+NDJSON) \u2502              \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n<p>No importa si llegas por CLI o por HTTP: el mismo codigo se ejecuta.\nEl bundle solo se encarga del transporte.<\/p>\n<h1 id=\"detalles-tecnicos\">Detalles tecnicos<\/h1>\n<ul>\n<li><strong>PHP &gt;= 7.4<\/strong>, Symfony 5.4 \/ 6.x \/ 7.x<\/li>\n<li><strong>NDJSON<\/strong> sobre HTTP (no WebSocket, no SSE).\n<code>fetch()<\/code> + <code>ReadableStream<\/code> +\n<code>TextDecoder<\/code><\/li>\n<li><strong>Web Component<\/strong> con Shadow DOM. Un unico fichero JS,\nzero deps<\/li>\n<li><strong>Auto-discovery<\/strong> via\n<code>bin\/console list --format=json<\/code> + <code>Process<\/code><\/li>\n<li><strong>Whitelist<\/strong> configurable. Solo los comandos que tu\npermitas se exponen<\/li>\n<li><strong>Overrides<\/strong> para convertir text inputs en dropdowns\n(ej: <code>--gateway: [stripe, paypal, braintree]<\/code>)<\/li>\n<li><strong>No incluye autenticacion<\/strong>. Tu pones tu\n<code>access_control<\/code>, firewall, VPN o lo que uses<\/li>\n<\/ul>\n<h1 id=\"instalacion-en-4-pasos\">Instalacion en 4 pasos<\/h1>\n<div class=\"sourceCode\" id=\"cb4\"><pre\nclass=\"sourceCode bash\"><code class=\"sourceCode bash\"><span id=\"cb4-1\"><a href=\"#cb4-1\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># 1. Instalar<\/span><\/span>\n<span id=\"cb4-2\"><a href=\"#cb4-2\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"ex\">composer<\/span> require pascualmg\/symfony-command-ui<\/span>\n<span id=\"cb4-3\"><a href=\"#cb4-3\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><\/span>\n<span id=\"cb4-4\"><a href=\"#cb4-4\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># 2. El bundle se auto-registra en bundles.php<\/span><\/span>\n<span id=\"cb4-5\"><a href=\"#cb4-5\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><\/span>\n<span id=\"cb4-6\"><a href=\"#cb4-6\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># 3. Importar rutas<\/span><\/span>\n<span id=\"cb4-7\"><a href=\"#cb4-7\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># config\/routes\/symfony_command_ui.yaml<\/span><\/span>\n<span id=\"cb4-8\"><a href=\"#cb4-8\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># symfony_command_ui:<\/span><\/span>\n<span id=\"cb4-9\"><a href=\"#cb4-9\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">#     resource: &#39;@SymfonyCommandUIBundle\/Resources\/config\/routes.php&#39;<\/span><\/span>\n<span id=\"cb4-10\"><a href=\"#cb4-10\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">#     prefix: \/symfony-console<\/span><\/span>\n<span id=\"cb4-11\"><a href=\"#cb4-11\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><\/span>\n<span id=\"cb4-12\"><a href=\"#cb4-12\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># 4. Configurar whitelist<\/span><\/span>\n<span id=\"cb4-13\"><a href=\"#cb4-13\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># config\/packages\/symfony_command_ui.yaml<\/span><\/span>\n<span id=\"cb4-14\"><a href=\"#cb4-14\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\"># symfony_command_ui:<\/span><\/span>\n<span id=\"cb4-15\"><a href=\"#cb4-15\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">#     allowed_commands:<\/span><\/span>\n<span id=\"cb4-16\"><a href=\"#cb4-16\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">#         - app:mi:comando<\/span><\/span>\n<span id=\"cb4-17\"><a href=\"#cb4-17\" aria-hidden=\"true\" tabindex=\"-1\"><\/a><span class=\"co\">#         - app:otro:comando<\/span><\/span><\/code><\/pre><\/div>\n<p>Abrir <code>https:\/\/tu-app.com\/symfony-console<\/code> y listo.<\/p>\n<h1 id=\"el-origen\">El origen<\/h1>\n<p>Esto surgio de un problema real: teniamos un microservicio de\nidentity con 6 comandos de consola para gestionar colectivos (una\nfeature B2B para un partner). Necesitabamos una UI para que el equipo\npudiera ejecutarlos sin SSH, y al mismo tiempo queriamos que los agentes\nde IA pudieran operar el flujo programaticamente.<\/p>\n<p>En vez de hacer un dashboard ad-hoc, extraje la logica generica en un\nbundle reutilizable. El prototipo funciono en una tarde. La lib se\npublico al dia siguiente. Hoy esta en Packagist.<\/p>\n<h1 id=\"links\">Links<\/h1>\n<ul>\n<li>Repositorio: <a\nhref=\"https:\/\/github.com\/pascualmg\/symfony-command-ui\">https:\/\/github.com\/pascualmg\/symfony-command-ui<\/a><\/li>\n<li>Packagist: <a\nhref=\"https:\/\/packagist.org\/packages\/pascualmg\/symfony-command-ui\">https:\/\/packagist.org\/packages\/pascualmg\/symfony-command-ui<\/a><\/li>\n<li>Licencia: MIT<\/li>\n<\/ul>\n","author":"Pascual","datePublished":"2026-04-14T00:00:00+00:00","orgSource":"#+TITLE: symfony-command-ui: tus comandos Symfony son tu MCP server\n#+AUTHOR: Pascual\n#+DATE: 2026-04-14\n\n* El problema que todos tenemos\n\nSi trabajas con Symfony, tienes comandos de consola. Muchos. ~app:users:sync~, ~app:payments:process~, ~app:cache:warmup~... Son la columna vertebral de tu aplicacion: encapsulan logica de negocio, se ejecutan en cron, los usas para debuggear, para migraciones, para todo.\n\nY sin embargo, ejecutarlos es un rollo:\n\n- SSH al servidor (o al pod de Kubernetes)\n- Recordar la sintaxis exacta (~--limit=100~ o ~--limit 100~?)\n- Copiar el output a mano si quieres compartirlo\n- Si estas en un pod de K8s, ni siquiera tienes terminal a veces\n\nY ahora, en la era agentica, hay un segundo problema: *como le dices a un agente de IA que ejecute tus comandos?* Los LLMs son buenos entendiendo JSON, haciendo HTTP, procesando streams. Pero no pueden hacer SSH a tu pod.\n\n* La solucion: un bundle que los expone a todos\n\n~symfony-command-ui~ es un bundle de Symfony que he publicado hoy. Hace una cosa simple:\n\n*Convierte tus comandos de consola en una UI web con terminal streaming Y en una API JSON que cualquier agente de IA puede usar.*\n\n#+begin_src bash\ncomposer require pascualmg\/symfony-command-ui\n#+end_src\n\nTres lineas de YAML y tienes un dashboard con todos tus comandos, auto-descubiertos, con formularios generados automaticamente desde tu ~InputDefinition~.\n\n* Como funciona\n\nLa arquitectura es simple:\n\n1. El bundle ejecuta ~php bin\/console list --format=json~ para descubrir los comandos disponibles\n2. Filtra por una whitelist que tu configuras (seguridad)\n3. Traduce cada ~InputOption~ y ~InputArgument~ a un formulario web (checkboxes, dropdowns, text inputs)\n4. Cuando pulsas Run, ejecuta el comando via ~Symfony\\Component\\Process~ y te streama el output en tiempo real como NDJSON\n\nCada comando se renderiza como una *card independiente* con su propio formulario y su propia terminal. Los outputs persisten: puedes ejecutar Stats mientras Generate JWT mantiene su resultado.\n\n* La parte interesante: interfaz dual\n\nAqui es donde se pone bueno. El bundle expone *dos interfaces con los mismos endpoints*:\n\n** Para humanos: el dashboard web\n\nUn Web Component (~<symfony-command>~) con Shadow DOM, zero dependencias, que descubre los comandos al montar y renderiza todo automaticamente. No hay npm, no hay webpack, no hay build step. Un unico fichero JS que el bundle sirve como asset.\n\nCSS customizable via custom properties. Dark theme por defecto. Theming completo si quieres.\n\n** Para agentes: la API JSON\n\nDos endpoints:\n\n- ~GET \/commands~: devuelve un JSON estructurado con todos los comandos disponibles, sus opciones, defaults y tipos\n- ~POST \/execute~: recibe ~{command, options}~ y streama el output como NDJSON\n\nCualquier agente que hable HTTP puede:\n\n1. Descubrir que operaciones existen en tu app\n2. Entender sus parametros (sin documentacion extra -- salen del ~InputDefinition~ de Symfony)\n3. Ejecutarlas y observar el resultado en tiempo real\n\nEs un *MCP server gratis*. Sin instalar nada especial. Sin protocolo custom. Solo HTTP + JSON.\n\n* Un ejemplo real: Claude operando una app\n\nImagina que tienes un e-commerce con estos comandos:\n\n#+begin_src yaml\nallowed_commands:\n    - app:orders:pending\n    - app:orders:process\n    - app:inventory:check\n    - app:reports:daily\n#+end_src\n\nUn agente de IA (Claude, GPT, tu propio agente) puede hacer:\n\n#+begin_quote\n*Tu*: \"Hay pedidos pendientes de mas de 500 euros?\"\n\n*Agente*: \/llama GET \/commands, encuentra app:orders:pending\/\n\/llama POST \/execute con {\"command\":\"app:orders:pending\",\"options\":{\"--min-amount\":500,\"--json\":true}}\/\n\n\"Si, hay 3 pedidos pendientes por encima de 500 euros: #4521 (720 euros), #4523 (1100 euros), #4529 (550 euros). Quieres que los procese?\"\n\n*Tu*: \"Primero con dry-run\"\n\n*Agente*: \/llama POST \/execute con {\"command\":\"app:orders:process\",\"options\":{\"--ids\":\"4521,4523,4529\",\"--dry-run\":true}}\/\n\n\"Dry run completado. Los 3 se procesarian correctamente. Total: 2370 euros. Ejecuto en real?\"\n#+end_quote\n\nTodo esto funciona porque tus comandos de Symfony *ya encapsulan la logica*. El bundle solo la expone.\n\n* El patron: logica en comandos, interfaz como adaptador\n\nSi haces DDD o hexagonal, tus comandos de Symfony son *adaptadores CLI* que llaman a use cases del dominio. Este bundle anade un segundo adaptador (HTTP) al mismo use case, sin tocar una linea de tu logica de negocio.\n\n#+begin_src\n                        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          bin\/console\u2500\u2500\u25ba\u2502              \u2502\n                        \u2502  Use Case    \u2502\n    symfony-command-ui\u2500\u2500\u25ba\u2502  (dominio)   \u2502\n          (HTTP+NDJSON) \u2502              \u2502\n                        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n#+end_src\n\nNo importa si llegas por CLI o por HTTP: el mismo codigo se ejecuta. El bundle solo se encarga del transporte.\n\n* Detalles tecnicos\n\n- *PHP >= 7.4*, Symfony 5.4 \/ 6.x \/ 7.x\n- *NDJSON* sobre HTTP (no WebSocket, no SSE). ~fetch()~ + ~ReadableStream~ + ~TextDecoder~\n- *Web Component* con Shadow DOM. Un unico fichero JS, zero deps\n- *Auto-discovery* via ~bin\/console list --format=json~ + ~Process~\n- *Whitelist* configurable. Solo los comandos que tu permitas se exponen\n- *Overrides* para convertir text inputs en dropdowns (ej: ~--gateway: [stripe, paypal, braintree]~)\n- *No incluye autenticacion*. Tu pones tu ~access_control~, firewall, VPN o lo que uses\n\n* Instalacion en 4 pasos\n\n#+begin_src bash\n# 1. Instalar\ncomposer require pascualmg\/symfony-command-ui\n\n# 2. El bundle se auto-registra en bundles.php\n\n# 3. Importar rutas\n# config\/routes\/symfony_command_ui.yaml\n# symfony_command_ui:\n#     resource: '@SymfonyCommandUIBundle\/Resources\/config\/routes.php'\n#     prefix: \/symfony-console\n\n# 4. Configurar whitelist\n# config\/packages\/symfony_command_ui.yaml\n# symfony_command_ui:\n#     allowed_commands:\n#         - app:mi:comando\n#         - app:otro:comando\n#+end_src\n\nAbrir ~https:\/\/tu-app.com\/symfony-console~ y listo.\n\n* El origen\n\nEsto surgio de un problema real: teniamos un microservicio de identity con 6 comandos de consola para gestionar colectivos (una feature B2B para un partner). Necesitabamos una UI para que el equipo pudiera ejecutarlos sin SSH, y al mismo tiempo queriamos que los agentes de IA pudieran operar el flujo programaticamente.\n\nEn vez de hacer un dashboard ad-hoc, extraje la logica generica en un bundle reutilizable. El prototipo funciono en una tarde. La lib se publico al dia siguiente. Hoy esta en Packagist.\n\n* Links\n\n- Repositorio: https:\/\/github.com\/pascualmg\/symfony-command-ui\n- Packagist: https:\/\/packagist.org\/packages\/pascualmg\/symfony-command-ui\n- Licencia: MIT\n"}