<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Khavel</title>
    <description>The latest articles on DEV Community by Khavel (@khavel).</description>
    <link>https://dev.to/khavel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F908485%2F809342dc-ba24-482e-a5b9-6ab3dbd61290.png</url>
      <title>DEV Community: Khavel</title>
      <link>https://dev.to/khavel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/khavel"/>
    <language>en</language>
    <item>
      <title>An embedding model's price is a one-time cost. Its dimensions are a subscription.</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:44:06 +0000</pubDate>
      <link>https://dev.to/khavel/an-embedding-models-price-is-a-one-time-cost-its-dimensions-are-a-subscription-2528</link>
      <guid>https://dev.to/khavel/an-embedding-models-price-is-a-one-time-cost-its-dimensions-are-a-subscription-2528</guid>
      <description>&lt;p&gt;Embedding models are billed per 1M input tokens, so that's the number that ends up in the comparison. Here are the nine generally-available embedding models that publish a token price, every rate re-read off the provider's own page this morning (2026-08-17):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;$/1M input&lt;/th&gt;
&lt;th&gt;Default dims&lt;/th&gt;
&lt;th&gt;Max input tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;amazon.titan-embed-text-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;text-embedding-v4&lt;/code&gt; (Qwen)&lt;/td&gt;
&lt;td&gt;Alibaba&lt;/td&gt;
&lt;td&gt;$0.07&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;embed-v4.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;128,000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text-embedding-3-large&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;$0.13&lt;/td&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nova-2-multimodal-embeddings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;$0.135&lt;/td&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-embedding-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codestral-embed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-embedding-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The Model column is the provider's own model string; the ids in our feed are normalised slightly differently — you'll see both at the end.)&lt;/p&gt;

&lt;p&gt;Top to bottom, that's a &lt;strong&gt;10x spread&lt;/strong&gt;. It is also, for most retrieval workloads, the least consequential number on the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ingestion bill is one-time, and it is small
&lt;/h2&gt;

&lt;p&gt;Take a corpus of 1,000,000 documents averaging 800 tokens. That's 800M tokens to embed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At $0.02/1M: &lt;strong&gt;$16.00&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;At $0.20/1M: &lt;strong&gt;$160.00&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire 10x spread is &lt;strong&gt;$144&lt;/strong&gt;, once. You will spend more than that deciding which model to use. The token price only becomes interesting again when you re-embed — a model migration, or a corpus that grows continuously — and even then it's a function of &lt;em&gt;new&lt;/em&gt; tokens, not of the corpus you already indexed.&lt;/p&gt;

&lt;p&gt;Meanwhile the thing you're actually buying is a pile of float vectors that you will store, index, and hold in RAM for as long as the product exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vector is what recurs
&lt;/h2&gt;

&lt;p&gt;One million vectors, float32:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimensions&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;12.29 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;6.14 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;4.10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;3.07 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;1.02 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'm deliberately not multiplying that by a vector-database rate — every managed vector store prices differently and I'm not going to invent a number. Use your own. The point is the shape: this one is &lt;strong&gt;monthly&lt;/strong&gt;, it scales linearly with the dimension count, and picking the 3072-dim model over the 1024-dim one triples it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  …except the dimension is usually a knob, not a spec
&lt;/h2&gt;

&lt;p&gt;This is the part that makes the comparison table misleading rather than merely incomplete. On most current models, the dimension count you see published is a &lt;strong&gt;default&lt;/strong&gt;, and you can ask for a smaller vector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; — a &lt;code&gt;dimensions&lt;/code&gt; request parameter, which its API reference says is supported on &lt;code&gt;text-embedding-3&lt;/code&gt; and later models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cohere&lt;/strong&gt; — &lt;code&gt;embed-v4.0&lt;/code&gt;'s docs table gives its dimensions as a choice of 256, 512, 1024 or 1536, with 1536 the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; — an &lt;code&gt;output_dimensionality&lt;/code&gt; parameter, via Matryoshka Representation Learning. Both Gemini embedding models default to 3072 and Google's docs say you can truncate below that without losing quality, recommending 768, 1536 or 3072.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon&lt;/strong&gt; — Titan Text Embeddings V2 is documented as having &lt;em&gt;"configurable output dimensions"&lt;/em&gt;, set via &lt;code&gt;dimensions&lt;/code&gt; in the request body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And Google publishes the trade-off as a table, which is the most useful thing I found all week. MTEB score by truncated dimension, for &lt;code&gt;gemini-embedding-001&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MRL dimension&lt;/th&gt;
&lt;th&gt;MTEB score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;td&gt;68.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68.17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;67.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512&lt;/td&gt;
&lt;td&gt;67.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;66.19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;63.31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that carefully. &lt;strong&gt;1536 scores fractionally higher than 2048.&lt;/strong&gt; Going from 1536 down to 768 costs &lt;strong&gt;0.18 MTEB points&lt;/strong&gt; and halves your storage, your index size and your RAM. The cliff doesn't arrive until 256, and it doesn't get steep until 128.&lt;/p&gt;

&lt;p&gt;So the honest version of "this model is 3072-dimensional" is "this model defaults to 3072 and its own vendor recommends 768 as a supported option". Those imply storage bills a factor of four apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that really is fixed: the input ceiling
&lt;/h2&gt;

&lt;p&gt;The context window is the field on these rows that you cannot negotiate, and it varies more than the other two. Cohere's own current lineup spans the entire range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;embed-english-v3.0&lt;/code&gt; — &lt;strong&gt;512&lt;/strong&gt; tokens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;embed-v4.0&lt;/code&gt; — &lt;strong&gt;128,000&lt;/strong&gt; tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's &lt;strong&gt;250x&lt;/strong&gt;, same vendor, both generally available today. Google moved from 2048 on &lt;code&gt;gemini-embedding-001&lt;/code&gt; to 8192 on &lt;code&gt;gemini-embedding-2&lt;/code&gt;. Most of the rest sit at 8192.&lt;/p&gt;

&lt;p&gt;Why it compounds: the ceiling caps your chunk size, chunk size sets your vector count, and vector count multiplied by dimensions is your storage &lt;em&gt;and&lt;/em&gt; your index. Chunking the same corpus against a 512-token ceiling instead of an 8192-token one yields &lt;strong&gt;16x the vectors&lt;/strong&gt;. Your token spend barely moves — it's the same text either way — while everything downstream of the embedding call multiplies.&lt;/p&gt;

&lt;p&gt;To be fair to the small-context models: almost nobody chunks at the ceiling, because retrieval quality usually wants smaller chunks than the maximum anyway. The ceiling doesn't dictate your chunk size. It removes options, and it's the only one of the three fields where the provider makes the decision for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rerank: there is no ladder to write
&lt;/h2&gt;

&lt;p&gt;I wanted to end with the same table for rerankers. It doesn't exist, and the reason is worth more than the table would have been.&lt;/p&gt;

&lt;p&gt;Of the 7 GA rerank models we track, &lt;strong&gt;3 publish a rate&lt;/strong&gt;, and the unit isn't tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Rerank v1&lt;/td&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$1.00&lt;/strong&gt; per 1,000 searches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rerank 4 Fast&lt;/td&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$2.00&lt;/strong&gt; per 1,000 searches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rerank 4 Pro&lt;/td&gt;
&lt;td&gt;Cohere&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$2.50&lt;/strong&gt; per 1,000 searches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A "search" is not a standard unit, and it is not a token. AWS's price book bills its reranker in &lt;em&gt;search units&lt;/em&gt; — $1 per 1,000 of them — where a unit is a query carrying some bounded number of document chunks, defined in the docs rather than on the price line. Cohere bills per 1,000 searches. Whether those two units mean the same thing for your query shape is your problem, not something either price implies.&lt;/p&gt;

&lt;p&gt;That AWS model is also region-scoped in a way the price book makes plain: the SKU (&lt;code&gt;Z7M6S4MRBXNXJRB4&lt;/code&gt;) is in &lt;code&gt;us-west-2&lt;/code&gt;, and there is no rerank SKU in the &lt;code&gt;us-east-1&lt;/code&gt; price book at all.&lt;/p&gt;

&lt;p&gt;The other four GA rerankers publish no first-party rate we could find. In our data those fields are &lt;code&gt;null&lt;/code&gt;, which is a fact about the provider, not a gap we're papering over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it as data
&lt;/h2&gt;

&lt;p&gt;All of the above is in a free JSON feed — no key, no signup, CORS open. (If you would rather just look at the 13 rows, they are on &lt;a href="https://aimodelwatch.dev/embeddings" rel="noopener noreferrer"&gt;one page&lt;/a&gt; too.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://aimodelwatch.dev/api/models.json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.models[]
      | select(.embedding_dimensions != null and .status == "ga")
      | [.price_input_per_mtok, .embedding_dimensions, .context_window, .id]
      | @tsv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output, run against the live endpoint today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.02    1024    8192    amazon-titan-embed-text-v2
0.02    1536    8192    text-embedding-3-small
0.07    1024    8192    qwen-text-embedding-v4
0.12    1536    128000  embed-v4-0
0.13    3072    8192    text-embedding-3-large
0.135   3072    8192    amazon-nova-2-multimodal-embeddings
0.15    1536    8192    codestral-embed
0.15    3072    2048    gemini-embedding-001
0.2     3072    8192    gemini-embedding-2
        384     512     embed-english-light-v3-0
        384     512     embed-multilingual-light-v3-0
        1024    512     embed-english-v3-0
        1024    512     embed-multilingual-v3-0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four rows with a blank price are Cohere's v3 embedding family. They're still GA and still documented; Cohere's pricing page currently lists only Embed 4 among its embedding models, so there is no first-party rate to carry. A blank there means "the provider doesn't publish this", not "we didn't look" — every row carries the &lt;code&gt;source_url&lt;/code&gt; it was read from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest note while you're looking at that output.&lt;/strong&gt; The two OpenAI rows read &lt;code&gt;8191&lt;/code&gt; when this was drafted, and I'd written 8192 in the table at the top. Both numbers are first-party: 8191 is the figure OpenAI's 2024 new-embedding-models announcement carried, and the current API reference states &lt;em&gt;"the max input tokens for the model (8192 tokens for all embedding models)"&lt;/em&gt;. The disagreement was reconciled to &lt;strong&gt;8192&lt;/strong&gt; on 2026-08-18 — the reference states the limit as a constraint on the &lt;code&gt;input&lt;/code&gt; parameter, i.e. it carries the field as data, while the model spec pages state no limit at all. It's one token out of eight thousand and it changes nothing in this article. I'm leaving the paragraph in because a catalog that quietly rounds its own disagreements away isn't worth querying — and because you can see the audit trail: the row's &lt;code&gt;notes&lt;/code&gt; field records the old value, the new one, and which surface won.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data from &lt;a href="https://aimodelwatch.dev" rel="noopener noreferrer"&gt;AI Model Watch&lt;/a&gt; — 231 models, prices and lifecycle dates read from official provider documentation, refreshed daily, with &lt;code&gt;null&lt;/code&gt; where the provider publishes nothing. Free JSON: &lt;a href="https://aimodelwatch.dev/api/models.json" rel="noopener noreferrer"&gt;&lt;code&gt;/api/models.json&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://aimodelwatch.dev/api/deprecations.json" rel="noopener noreferrer"&gt;&lt;code&gt;/api/deprecations.json&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>embeddings</category>
      <category>rag</category>
      <category>api</category>
    </item>
    <item>
      <title>OpenAI Responses API: function calling fiable, estado y trabajos en segundo plano</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Mon, 17 Aug 2026 09:25:11 +0000</pubDate>
      <link>https://dev.to/khavel/openai-responses-api-function-calling-fiable-estado-y-trabajos-en-segundo-plano-3a0g</link>
      <guid>https://dev.to/khavel/openai-responses-api-function-calling-fiable-estado-y-trabajos-en-segundo-plano-3a0g</guid>
      <description>&lt;p&gt;Responses API no convierte una función en fiable por sí sola. Esta guía muestra el bucle correcto de tool calls, validación, estado y trabajos largos para que un agente no confunda una respuesta convincente con una acción segura.&lt;/p&gt;

&lt;p&gt;OpenAI Responses API es la interfaz unificada para generar respuestas, usar herramientas y conservar estado entre turnos. Un tool call no es una orden que el servidor deba obedecer: es una propuesta del modelo que tu backend debe autorizar, validar, ejecutar de forma idempotente y devolver al modelo como &lt;code&gt;function_call_output&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;OpenAI Responses API&lt;/code&gt;. La intención es técnica: un developer que ya puede hacer una llamada básica necesita montar un flujo fiable con function calling, JSON estructurado, streaming, estado conversacional y trabajos que no caben en una petición HTTP corta.&lt;/p&gt;

&lt;p&gt;Mi postura: empieza con Responses API antes de introducir una capa de agentes. Es un contrato explícito que te obliga a entender input, output, tools y estado. Un SDK de agentes puede ahorrar orquestación después; no debería ocultar permisos, validación ni efectos externos.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qué es Responses API y qué no resuelve
&lt;/h3&gt;

&lt;p&gt;Responses API crea un objeto &lt;code&gt;response&lt;/code&gt; a partir de un modelo, una entrada y, opcionalmente, herramientas. La salida no tiene por qué ser texto: puede contener mensajes, llamadas de función, resultados de herramientas alojadas, elementos de razonamiento y eventos de streaming. Leer solo &lt;code&gt;output_text&lt;/code&gt; es correcto para un chat simple, pero insuficiente para un flujo que actúa sobre sistemas reales.&lt;/p&gt;

&lt;p&gt;La API puede encadenar contexto con &lt;code&gt;previous_response_id&lt;/code&gt; o con Conversations. Eso evita reenviar un historial manual enorme, pero no sustituye tu modelo de negocio: tú decides qué conversación pertenece a qué usuario, cuánto vive, qué datos se permiten y cuándo hay que resumir o borrar estado.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tampoco decide si una llamada es segura. El modelo puede proponer &lt;code&gt;create_invoice&lt;/code&gt;, &lt;code&gt;send_email&lt;/code&gt; o &lt;code&gt;deploy&lt;/code&gt;. Tu aplicación sigue siendo el control de autoridad: autentica al usuario, limita el recurso, valida argumentos, exige aprobación cuando corresponde y registra el efecto final.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8txhhk48bnrn79gls1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8txhhk48bnrn79gls1o.png" alt="Diagrama conceptual de un usuario que envía una petición al orquestador de Responses API; el flujo se divide entre una tool validada, salida JSON estructurada y un trabajo asíncrono, con una barrera de autorización, registro de auditoría y estado separado" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El modelo propone pasos; el backend conserva la autoridad. Estado, validación, colas y auditoría son piezas distintas del texto generado.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  El bucle correcto de function calling
&lt;/h3&gt;

&lt;p&gt;Function calling tiene cuatro etapas: declaras una tool con un schema; el modelo emite uno o más elementos &lt;code&gt;function_call&lt;/code&gt;; tu servidor valida y ejecuta solo los que autoriza; devuelves un &lt;code&gt;function_call_output&lt;/code&gt; con el &lt;code&gt;call_id&lt;/code&gt; original y pides la siguiente respuesta. Si falta el último paso, el modelo no ve el resultado real de la acción y tenderá a completar la conversación con una suposición.&lt;/p&gt;

&lt;p&gt;No ejecutes argumentos directamente con &lt;code&gt;json.loads&lt;/code&gt; y una llamada a tu SDK interno. El schema reduce salidas mal formadas, pero no prueba que el usuario tenga acceso a &lt;code&gt;project_id&lt;/code&gt;, que una fecha exista ni que la acción sea razonable. Valida tipos, rangos, pertenencia al tenant y política de negocio fuera del modelo.&lt;/p&gt;

&lt;p&gt;Para una operación con efecto, asocia una clave idempotente a la intención de negocio, no al texto del modelo. Un retry HTTP, una reconexión de streaming o una segunda respuesta no debe enviar dos emails o crear dos facturas. Guarda &lt;code&gt;call_id&lt;/code&gt;, usuario, recurso, hash del payload y resultado de la ejecución.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Código: tool estrecha y resultado verificable en Python
&lt;/h3&gt;

&lt;p&gt;Este ejemplo ilustra el bucle. La tool es deliberadamente de lectura y el resultado vuelve como datos, no como instrucciones. En producción, &lt;code&gt;get_release&lt;/code&gt; debería imponer autorización y recuperar solo los campos permitidos para el usuario autenticado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;responses_tools.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_release&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Returns approved release metadata for one repository.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minLength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_release_for_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;allowed_repos_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# authz, not a model prompt
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;read_release_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;¿Cuál es el último release de api-gateway?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tool_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_release&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_release_for_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;tool_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;previous_response_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El detalle importante no es el nombre de la función: es que &lt;code&gt;allowed_repos_for&lt;/code&gt; vive en tu backend. Si el modelo propone otro repo, la autorización falla antes de tocar la fuente de datos. Devuelve un error de dominio breve y deja que el modelo explique el límite al usuario, en vez de darle una excepción cruda o inventar una respuesta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Outputs: contrato de interfaz, no control de seguridad
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cuando necesitas una salida que otro sistema consuma, usa Structured Outputs con JSON Schema estricto. En Responses API el formato se configura dentro de &lt;code&gt;text.format&lt;/code&gt;; para tools, define &lt;code&gt;strict: true&lt;/code&gt; y limita propiedades. Eso hace que el contrato sea más predecible que pedir «devuelve JSON válido» en un prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Un schema debe representar una decisión pequeña y verificable. Para triage, por ejemplo: categoría de una allowlist, confianza acotada, evidencia citada y &lt;code&gt;needs_human_review&lt;/code&gt;. Evita un objeto genérico tipo &lt;code&gt;action: string&lt;/code&gt; que luego se convierte en una puerta trasera de comandos para cualquier integración.&lt;/p&gt;

&lt;p&gt;Trata cualquier campo generado como entrada no confiable al cruzar una frontera. &lt;code&gt;strict&lt;/code&gt; evita muchas formas inválidas; no sustituye escape HTML, validación de URLs, autorización, control de concurrencia, límites de tamaño ni saneamiento para SQL o shell. Un JSON impecable puede describir una acción equivocada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estado: previous_response_id frente a Conversations
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;previous_response_id&lt;/code&gt; es útil para enlazar el siguiente turno al anterior con una relación explícita. Es cómodo en una conversación corta o en un workflow donde tu base de datos guarda el último response ID por sesión. Las instrucciones de una llamada anterior no se arrastran automáticamente si pasas instrucciones nuevas: verifica ese comportamiento antes de asumir que una política quedó vigente.&lt;/p&gt;

&lt;p&gt;Conversations es una entidad de estado reutilizable para añadir y recuperar ítems entre respuestas. Encaja cuando necesitas una conversación estable que pueda sobrevivir a distintos dispositivos o workers. Aun así, no conviertas la Conversation en tu única fuente de verdad: conserva en tu base la identidad del usuario, el tenant, el estado de aprobación y referencias de auditoría.&lt;/p&gt;

&lt;p&gt;Mi regla: guarda solo IDs y contexto mínimo de producto; vuelve a resolver permisos, herramientas permitidas y policy en cada petición. El estado puede recordar la conversación, pero no debe heredar autoridad. Un usuario que pierde acceso a un proyecto no debería mantenerlo porque una conversación vieja lo mencionaba.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Streaming y background mode son flujos distintos
&lt;/h3&gt;

&lt;p&gt;Streaming usa eventos server-sent para pintar progreso o texto parcial con baja latencia. Es una decisión de experiencia de usuario; no conviertas cada delta en un registro de negocio ni ejecutes una tool al primer fragmento. Espera al elemento de function call completo y conserva una ruta clara de cancelación del cliente.&lt;/p&gt;

&lt;p&gt;Background mode sirve para respuestas largas que deben continuar aunque la petición web se corte. Creas la respuesta con &lt;code&gt;background=true&lt;/code&gt;, persistes su ID y consultas su estado o recibes el evento webhook correspondiente. El frontend no debería mantener una conexión abierta durante minutos solo para fingir que un job asíncrono es streaming.&lt;/p&gt;

&lt;p&gt;La consecuencia operativa importa: background mode conserva datos para poder hacer polling y no es compatible con Zero Data Retention. Revisa data controls, retención y la región de datos de tu organización antes de activarlo en flujos con información sensible. Para una tarea larga sin datos que deban salir, una cola propia y una llamada normal puede ser una alternativa más controlable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Herramientas alojadas, MCP y límites de datos
&lt;/h3&gt;

&lt;p&gt;Responses API puede combinar funciones de tu aplicación con herramientas alojadas, como web search, file search, code interpreter o image generation, según modelo y disponibilidad. Cada una introduce otra frontera: cuota, tiempo, datos enviados y resultados que pueden estar equivocados o contener instrucciones externas.&lt;/p&gt;

&lt;p&gt;Los servidores MCP remotos son servicios de terceros. No les pases un token o documento solo porque una tool description sea atractiva. Delimita por servidor la URL, identidad, scopes, datos que puede recibir, rate limits y la aprobación para efectos externos. MCP conecta capacidades; no valida automáticamente su confianza.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Usa &lt;code&gt;allowed_tools&lt;/code&gt; o un conjunto de tools por tarea cuando sea posible. Un agente de triage no necesita la misma superficie que uno de release. Reducir opciones también mejora la calidad: al modelo le cuesta menos elegir una tool cuando no le ofreces quince acciones parecidas con permisos distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arquitectura mínima que llevaría a producción
&lt;/h2&gt;

&lt;p&gt;Entrada autenticada → policy de tenant → creación de response → parser de output → validador de schema y negocio → executor idempotente → auditoría → &lt;code&gt;function_call_output&lt;/code&gt; → respuesta final. Si hay una acción sensible, añade una transición explícita de propuesta a aprobación: el modelo prepara payload y evidencia; una persona o regla independiente habilita la mutación.&lt;/p&gt;

&lt;p&gt;Mantén los executors fuera del prompt. Una tool debería ser una función estrecha, con nombre que explique el efecto, parámetros mínimos y un resultado redactado. &lt;code&gt;update_customer&lt;/code&gt; es demasiado grande; &lt;code&gt;propose_customer_address_change&lt;/code&gt; y &lt;code&gt;apply_approved_address_change&lt;/code&gt; dejan una frontera revisable.&lt;/p&gt;

&lt;p&gt;Mide más que éxito HTTP: porcentaje de tool calls válidas, denegadas por policy, reintentos idempotentes, aprobaciones, errores por tipo, latencia p50/p95, coste por workflow y tareas resueltas sin escalado. Una respuesta fluida puede ocultar que el modelo llama tres veces a una API o que el 20% de acciones queda bloqueado al final.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Checklist antes de habilitar una tool con efecto
&lt;/h3&gt;

&lt;p&gt;La tool expresa una única capacidad y no acepta campos libres que acaben en SQL, shell o URLs arbitrarias.&lt;/p&gt;

&lt;p&gt;El backend autentica al usuario y comprueba autorización por tenant, recurso y operación; el modelo no decide permisos.&lt;/p&gt;

&lt;p&gt;Los argumentos pasan JSON Schema y validación de negocio antes de llegar a un executor.&lt;/p&gt;

&lt;p&gt;Las mutaciones tienen una clave idempotente y un registro de resultado por operación de negocio.&lt;/p&gt;

&lt;p&gt;Las acciones externas o irreversibles muestran destino, payload, evidencia y riesgo antes de la aprobación.&lt;/p&gt;

&lt;p&gt;Las tools disponibles se reducen por tarea y se revisan al cambiar de modelo, prompt o integración.&lt;/p&gt;

&lt;p&gt;El estado conversacional no concede permisos persistentes y tiene una política de retención explícita.&lt;/p&gt;

&lt;p&gt;Streaming, background jobs y webhooks tienen timeouts, cancelación, reintentos y observabilidad propios.&lt;/p&gt;

&lt;p&gt;Hay evals con entradas ambiguas, argumentos inválidos, recursos de otro tenant y prompt injection indirecta.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;Responses API es una buena base cuando quieres control fino: te enseña exactamente cuándo el modelo habló, cuándo pidió una tool y cuándo tu sistema produjo un resultado verificable. Esa claridad vale más que una demo de agente que parece autónoma hasta que intenta escribir en producción.&lt;/p&gt;

&lt;p&gt;Empieza por una tool de lectura, un schema pequeño y una traza completa. Añade estado cuando haya una razón de producto, y background mode cuando el trabajo de verdad sea largo. La autonomía útil no consiste en dar más funciones al modelo: consiste en hacer que cada capacidad tenga una frontera, una evidencia y una forma segura de fallar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es OpenAI Responses API?
&lt;/h3&gt;

&lt;p&gt;Es la API unificada de OpenAI para crear respuestas con input multimodal, herramientas, streaming y estado conversacional. La salida puede incluir texto y elementos de tool calling, no solo una cadena.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Responses API sustituye a OpenAI Agents SDK?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. Responses API ofrece el contrato de bajo nivel; Agents SDK puede ayudar a orquestar agentes. Si necesitas permisos y efectos controlados, debes implementar validación y autorización en cualquiera de las dos capas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Function calling ejecuta mi función automáticamente?
&lt;/h3&gt;

&lt;p&gt;No. El modelo devuelve una propuesta de llamada; tu aplicación interpreta el output, valida argumentos y permisos, ejecuta si procede y devuelve un &lt;code&gt;function_call_output&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Para qué sirve previous_response_id?
&lt;/h3&gt;

&lt;p&gt;Enlaza una respuesta nueva con el contexto de la respuesta anterior. Es útil para turnos cortos, pero no sustituye una política de identidad, autorización o retención de datos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo uso background mode?
&lt;/h3&gt;

&lt;p&gt;Cuando una respuesta puede durar más que la petición HTTP normal y quieres consultar su estado o recibir un webhook. Revisa antes su efecto en retención de datos y compatibilidad con Zero Data Retention.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Structured Outputs hace segura una acción?
&lt;/h3&gt;

&lt;p&gt;No. Hace más predecible el formato. Todavía debes validar negocio, scopes, tenant, recursos, límites, idempotencia y aprobación humana cuando exista efecto externo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo llevar una tool de Responses API de demo a producción
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elegir una capacidad de lectura.&lt;/strong&gt; Empieza por una consulta reversible con un recurso claro, como recuperar metadata de un release aprobado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diseñar el schema.&lt;/strong&gt; Declara campos mínimos, tipos, allowlists y &lt;code&gt;additionalProperties: false&lt;/code&gt;; activa modo estricto cuando sea compatible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separar autorización.&lt;/strong&gt; Resuelve usuario, tenant, scopes y recurso en el backend antes de llamar a la fuente de datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crear el primer response.&lt;/strong&gt; Envía el input y solo las tools necesarias para esa tarea; registra el response ID y la versión de policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interpretar function calls.&lt;/strong&gt; Procesa únicamente elementos completos de tipo function call; no ejecutes texto libre ni deltas de streaming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validar y ejecutar.&lt;/strong&gt; Comprueba schema y reglas de negocio, aplica rate limits e idempotencia y captura un resultado redactado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devolver function_call_output.&lt;/strong&gt; Usa el call ID original y datos estructurados para que el siguiente response pueda explicar el resultado real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Añadir aprobación.&lt;/strong&gt; Separa propuesta y mutación cuando la acción escriba, envíe, despliegue o transfiera información.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preparar fallos.&lt;/strong&gt; Define errores de autorización, validación, proveedor y timeout; cada uno debe tener una recuperación distinta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir y evaluar.&lt;/strong&gt; Prueba tenants cruzados, argumentos hostiles y retries; mide tools inválidas, bloqueos, coste, latencia y resolución.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/api-reference/responses" rel="noopener noreferrer"&gt;OpenAI API: Responses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI API: function calling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/structured-outputs" rel="noopener noreferrer"&gt;OpenAI API: Structured Outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/conversation-state" rel="noopener noreferrer"&gt;OpenAI API: conversation state&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/background" rel="noopener noreferrer"&gt;OpenAI API: background mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/streaming-responses" rel="noopener noreferrer"&gt;OpenAI API: streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/tools" rel="noopener noreferrer"&gt;OpenAI API: built-in tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/guides/your-data" rel="noopener noreferrer"&gt;OpenAI API: data controls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/openai-agents-sdk-mcp-guardrails-tracing/" rel="noopener noreferrer"&gt;OpenAI Agents SDK: MCP, guardrails y tracing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/evaluacion-rag-produccion-metricas-datasets/" rel="noopener noreferrer"&gt;Evaluación RAG en producción&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para observar agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad, permisos y supply chain&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>MCP Registry: cómo publicar y descubrir servidores MCP sin confiar a ciegas</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:48:35 +0000</pubDate>
      <link>https://dev.to/khavel/mcp-registry-como-publicar-y-descubrir-servidores-mcp-sin-confiar-a-ciegas-874</link>
      <guid>https://dev.to/khavel/mcp-registry-como-publicar-y-descubrir-servidores-mcp-sin-confiar-a-ciegas-874</guid>
      <description>&lt;p&gt;El MCP Registry mejora el descubrimiento de servidores, no certifica que sean seguros. Aprende a publicar metadata reproducible y a construir una allowlist interna que trate cada servidor como una dependencia con privilegios.&lt;/p&gt;

&lt;p&gt;Un MCP Registry es un catálogo con una API estándar para describir y descubrir servidores MCP. El registro oficial publica metadata —nombre, versión, repositorio, paquete o endpoint remoto—; no hospeda tu binario ni convierte un servidor listado en seguro o adecuado para tu empresa.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;MCP Registry&lt;/code&gt;. La intención es técnica y práctica: un maintainer quiere publicar un servidor reproducible, y un equipo quiere descubrirlo sin transformar una búsqueda de herramientas en una puerta de entrada a paquetes y credenciales no revisados.&lt;/p&gt;

&lt;p&gt;Mi postura: usa el registro público como inventario y canal de distribución de metadata, no como una lista de confianza. La unidad de confianza sigue siendo una versión concreta de un artefacto, su código, sus tools, su identidad de ejecución y los permisos que le concedes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qué es MCP Registry y qué problema resuelve
&lt;/h3&gt;

&lt;p&gt;MCP Registry es la especificación y el ecosistema de registros para servidores Model Context Protocol. El Official MCP Registry, en &lt;code&gt;registry.modelcontextprotocol.io&lt;/code&gt;, es un catálogo público de metadata y una API REST sobre la que pueden construirse marketplaces o sub-registros. Su valor es que un cliente no tenga que adivinar cómo encontrar, instalar o actualizar cada integración.&lt;/p&gt;

&lt;p&gt;La frase importante es metadata. Un &lt;code&gt;server.json&lt;/code&gt; puede apuntar a un paquete npm, PyPI, una imagen OCI o un endpoint remoto, junto a los transportes y la configuración de arranque. El registro no ejecuta ese servidor por ti ni inspecciona exhaustivamente lo que hará cuando tenga acceso a tu filesystem, red, OAuth o secretos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eso separa tres cosas que se confunden con facilidad: descubrimiento (encontrar una ficha), procedencia (saber quién puede publicar un namespace) y confianza operativa (decidir si esta versión recibe permisos en tu entorno). El registro ayuda mucho con las dos primeras; la tercera es una política tuya.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkm6jjzodhavccrllxhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkm6jjzodhavccrllxhs.png" alt="Diagrama conceptual que conecta código y paquete, metadata server.json, registro MCP público, allowlist privada y hosts de desarrollo; debajo aparecen controles de identidad, integridad, sandbox, aprobación y auditoría" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Un registro público resuelve discovery; la allowlist y los controles de ejecución resuelven el riesgo de introducir una nueva dependencia con capacidades de agente.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  El modelo mental correcto: catálogo, no sello de seguridad
&lt;/h3&gt;

&lt;p&gt;Que un servidor aparezca en un registro oficial no significa que sus dependencias sean benignas, que el maintainer siga controlando el paquete, que sus tool descriptions no hayan cambiado o que encaje con tus datos. La propia documentación lo presenta como un repositorio de información autodeclarada y, mientras siga en preview, avisa de posibles cambios incompatibles o resets de datos.&lt;/p&gt;

&lt;p&gt;Trátalo como tratarías npm: una ficha reduce fricción de discovery y aporta campos comparables; no sustituye revisión de código, lockfile, análisis de dependencias, firma, sandbox o permisos mínimos. En MCP el impacto puede ser mayor que en una librería de UI porque el proceso puede recibir secretos y ejecutar operaciones en nombre de un usuario.&lt;/p&gt;

&lt;p&gt;Una política sana empieza con esta pregunta: ¿qué puede leer, escribir, ejecutar o enviar este servidor después de instalarse? Si no puedes responderla para una versión fijada, no está listo para la allowlist, aunque tenga un nombre bonito, muchos installs o una referencia en un marketplace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  server.json: el contrato que publicas
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;server.json&lt;/code&gt; es la ficha versionada del servidor. Como mínimo declara un nombre único, descripción, versión, repositorio y una o más formas de distribución: &lt;code&gt;packages&lt;/code&gt; para artefactos instalables o &lt;code&gt;remotes&lt;/code&gt; para endpoints. Para un paquete también declara su &lt;code&gt;registryType&lt;/code&gt;, identificador, versión y transporte; para un remoto, URL y transporte compatible.&lt;/p&gt;

&lt;p&gt;No copies un ejemplo antiguo sin comprobar el schema que genera tu versión de &lt;code&gt;mcp-publisher&lt;/code&gt;. El formato evoluciona durante preview. La forma menos frágil de empezar es &lt;code&gt;mcp-publisher init&lt;/code&gt;, revisar el JSON resultante y validarlo en CI contra el schema actual antes de publicar. El contrato de registry no es el archivo de configuración con secretos que ejecuta el host.&lt;/p&gt;

&lt;p&gt;Un ejemplo deliberadamente mínimo para un paquete npm por STDIO sería este. Sustituye los nombres, controla la versión desde tu release y no incluyas valores de secretos: la ficha solo puede describir variables requeridas, no contenerlas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;server.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.github.acme/release-notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MCP server for approved release-note data."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/acme/release-notes-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"registryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@acme/release-notes-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mantén la descripción factual y breve. También es entrada para hosts y modelos: una descripción ambigua, promocional o con instrucciones operativas largas aumenta el riesgo de que un agente elija una capability que no debería tener.&lt;/p&gt;

&lt;h2&gt;
  
  
  Namespace y procedencia: quién puede afirmar ese nombre
&lt;/h2&gt;

&lt;p&gt;El registro oficial asocia la publicación a un namespace. Para &lt;code&gt;io.github.*&lt;/code&gt; usa identidad de GitHub; para dominios propios puede verificar DNS o HTTP. Esa verificación evita que cualquiera publique bajo &lt;code&gt;com.tuempresa.*&lt;/code&gt;, pero no demuestra que todo el código de un repositorio o paquete sea seguro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Elige un namespace que sobreviva a cambios de equipo. Si tu servidor es producto de una organización, un namespace de dominio verificado suele expresar mejor la propiedad que una cuenta personal. Documenta qué repositorio, pipeline y equipo pueden publicar y elimina permisos cuando alguien deja el proyecto.&lt;/p&gt;

&lt;p&gt;En CI, separa el token que publica el artefacto del mecanismo que publica la metadata. El quickstart del registro ofrece autenticación GitHub/OIDC; úsala para que el pipeline pueda probar origen sin guardar una sesión humana de larga duración. Protege la rama y exige revisión del cambio de &lt;code&gt;server.json&lt;/code&gt;, igual que harías con un workflow de release.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Versionado inmutable: publica un release, no una corrección silenciosa
&lt;/h2&gt;

&lt;p&gt;Cada publicación de un servidor necesita una versión única. Una vez publicada, la metadata de esa versión es inmutable; si debes corregir descripción, repositorio, paquete o endpoint, publica otra versión. El registro intenta ordenar SemVer y marca la versión apropiada como &lt;code&gt;latest&lt;/code&gt;, por lo que usar &lt;code&gt;1.4.0&lt;/code&gt; de forma consistente simplifica a clientes y humanos.&lt;/p&gt;

&lt;p&gt;No uses &lt;code&gt;latest&lt;/code&gt; como versión de paquete en una allowlist. Fija la versión del artefacto y conserva su integridad en un lockfile, digest OCI o checksum cuando aplique. &lt;code&gt;latest&lt;/code&gt; del registry es una conveniencia de discovery, no una orden para actualizar procesos de desarrollo sin revisar qué cambió.&lt;/p&gt;

&lt;p&gt;Cuando solo ajustes metadata, una prerelease de registry puede ser preferible a fingir que el binario cambió. Pero no ocultes una modificación real de tools o permisos detrás de un parche menor: para el consumidor, añadir &lt;code&gt;delete_repository&lt;/code&gt; es un cambio de riesgo aunque tu API siga siendo compatible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Publicar paso a paso desde CI
&lt;/h3&gt;

&lt;p&gt;El orden seguro es: compilar, probar y publicar el artefacto; verificar que es recuperable por su identificador y versión; generar o actualizar &lt;code&gt;server.json&lt;/code&gt;; validar schema y coherencia; autenticar el publisher con identidad de CI; publicar la metadata; y consultar la API para confirmar que la versión concreta aparece. Si publicas la ficha antes que el paquete, invitas a instalaciones rotas.&lt;/p&gt;

&lt;p&gt;Para npm, el registro pide que el paquete se vincule a su nombre MCP mediante &lt;code&gt;mcpName&lt;/code&gt;. Esa comprobación reduce la distancia entre metadata y paquete. Añade además tests que arranquen el paquete exactamente como lo describe el &lt;code&gt;server.json&lt;/code&gt;: comando, transporte, variables declaradas y un &lt;code&gt;initialize&lt;/code&gt; de prueba sin tocar datos reales.&lt;/p&gt;

&lt;p&gt;Un esqueleto de workflow puede ser tan simple como el siguiente. No es una receta para copiar secretos: el token OIDC y los permisos exactos dependen de tu proveedor y del namespace. La parte importante es que publicación sea una consecuencia de artefacto probado, no un comando manual desde un portátil.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;release.sh (esquema)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci
npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm publish &lt;span class="nt"&gt;--access&lt;/span&gt; public

node scripts/assert-server-json.mjs server.json
mcp-publisher login github-oidc
mcp-publisher publish server.json

curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt;   &lt;span class="s2"&gt;"https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.acme/release-notes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Haz que &lt;code&gt;assert-server-json&lt;/code&gt; compare &lt;code&gt;name&lt;/code&gt;, versión, repositorio y paquete contra &lt;code&gt;package.json&lt;/code&gt; y la etiqueta Git. Es una comprobación pequeña que evita el fallo más tonto del ecosistema: publicar metadata de &lt;code&gt;1.4.0&lt;/code&gt; que instala sin querer &lt;code&gt;1.3.2&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Consumir la API sin mezclar discovery y ejecución
&lt;/h3&gt;

&lt;p&gt;La API v0.1 expone una lista de servidores y el detalle de una versión. Permite filtrar por &lt;code&gt;search&lt;/code&gt;, pedir solo &lt;code&gt;latest&lt;/code&gt; o sincronizar incrementalmente con &lt;code&gt;updated_since&lt;/code&gt;. Esto es suficiente para construir una vista de catálogo o un job que detecte cambios; no lo conviertas en un instalador automático para cada resultado nuevo.&lt;/p&gt;

&lt;p&gt;El patrón adecuado es ingestión → normalización → evaluación de política → aprobación → distribución. Tu job puede traer nuevas fichas a una base interna y marcar qué cambió, pero un servidor no pasa a ser ejecutable por un developer hasta que una persona o una regla verificable aprueba su versión, identidad, permisos y distribución.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Guarda la versión y la respuesta original que revisaste. Si el upstream se actualiza, compara el nombre del paquete, transporte, comando, URL, variables, tools observadas y permisos. Un cambio en cualquiera de ellos requiere reevaluación; no basta con que &lt;code&gt;version=latest&lt;/code&gt; avance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sub-registro privado: la capa que una empresa realmente necesita
&lt;/h2&gt;

&lt;p&gt;El registro oficial es para servidores públicamente accesibles; para un servicio interno o una dependencia aprobada solo para tu organización, crea un sub-registro privado o un catálogo compatible. GitHub documenta que una implementación v0.1 necesita endpoints de listado y detalle, además de CORS si un cliente lo consume desde navegador o IDE.&lt;/p&gt;

&lt;p&gt;El sub-registro no tiene que duplicar toda la funcionalidad del público. Empieza con una allowlist inmutable y explícita: ID interno, server name upstream, versión exacta, fuente, owner, clasificación de datos, scopes permitidos, transporte, fecha de revisión y fecha de caducidad. Si falta owner o fecha, el ítem caduca en vez de quedarse como excepción eterna.&lt;/p&gt;

&lt;p&gt;Puedes sincronizar fichas públicas como candidatos, pero no copies automáticamente todas. La ganancia real es cambiar la experiencia por defecto: el developer descubre únicamente servidores aprobados, y el host impide conexiones fuera de política cuando la plataforma lo permita.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Supply chain MCP: controles antes de dar una tool al modelo
&lt;/h3&gt;

&lt;p&gt;Antes de instalar un paquete local, verifica el publisher, repositorio y artefacto; fija versión y lockfile; analiza dependencias; ejecuta el proceso con filesystem, red y variables mínimas; y revisa el comando que el host va a lanzar completo. La recomendación de OWASP es clara: un servidor MCP local puede convertirse en una vía de sandbox escape, exfiltración o ejecución arbitraria si recibe acceso total por comodidad.&lt;/p&gt;

&lt;p&gt;Después de instalar, inspecciona también las tools: nombre, descripción, argumentos, outputs y destino. Las descripciones y schemas son superficie de prompt injection. Conserva un hash de la definición de tools que aprobaste y alerta si cambia; un servidor que hoy solo lee puede sufrir un rug pull mañana sin que cambie el nombre del paquete.&lt;/p&gt;

&lt;p&gt;Para servidores remotos, añade otra capa: validación TLS, URL exacta, OAuth con audiencia y scopes estrechos, egress controlado y rate limits. Una ficha de registry puede hacer visible un endpoint; no concede a ese endpoint derecho a recibir tokens de tu usuario. Para el flujo OAuth completo, consulta nuestra guía de OAuth 2.1 para MCP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  De la ficha al host: consentimiento y aislamiento
&lt;/h3&gt;

&lt;p&gt;El host debe enseñar qué se instala o conecta, qué comando se ejecutará en local, qué variables necesita y qué acciones expone. La aprobación del usuario no puede ser una tarjeta truncada con un botón «Conectar». Si el host no revela el comando completo, los permisos o la procedencia, el equipo pierde la evidencia necesaria para aprobarlo.&lt;/p&gt;

&lt;p&gt;Aísla servidores como dominios de seguridad independientes. Un servidor de documentación no necesita el token de un servidor de deploy ni acceso a todos los archivos del repositorio. Da una credencial por servidor y entorno, monta solo los directorios imprescindibles y bloquea red saliente salvo destinos que puedas justificar.&lt;/p&gt;

&lt;p&gt;Las mutaciones de impacto —escribir código, emitir una orden, cambiar un permiso, enviar datos fuera— deben seguir requiriendo aprobación con parámetros completos. Un registro resuelve cómo encontrar una tool; no decide cuándo un agente puede ejecutar una acción con consecuencias.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observabilidad y renovación de confianza
&lt;/h2&gt;

&lt;p&gt;Registra server name, versión, digest o lockfile, host, usuario o service account, tool, argumentos redactados, resultado, latencia y decisión de aprobación. Sin esa relación no podrás responder qué servidor consultó un dato o cambió un recurso cuando una alerta llegue semanas después.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Configura dos bucles de revisión. El primero es de cambios: nueva versión, nuevo paquete, endpoint, comando, tool o scope reabre la evaluación. El segundo es temporal: cada entrada aprobada expira en 90 o 180 días y necesita un owner que confirme que sigue mantenida y con el mismo riesgo aceptable.&lt;/p&gt;

&lt;p&gt;Mide también fricción útil: solicitudes de alta, tiempo hasta revisión, instalaciones rechazadas, permisos denegados, tools poco usadas y cambios detectados. Si tu catálogo tarda semanas para un servidor de lectura de bajo riesgo, acabará apareciendo un bypass; si aprueba todo en cinco minutos, solo has creado una lista decorativa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de publicación y consumo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El nombre MCP pertenece a un namespace que controlas y la identidad de CI puede probarlo.&lt;/li&gt;
&lt;li&gt;Paquete o endpoint existen antes que la ficha y su versión coincide exactamente con &lt;code&gt;server.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El schema se valida en CI y el proceso se arranca en una prueba de integración sin secretos reales.&lt;/li&gt;
&lt;li&gt;Cada publicación usa una versión única; una corrección se publica como release nuevo, no se reescribe.&lt;/li&gt;
&lt;li&gt;La ficha no incluye secretos ni se confunde con el archivo de configuración runtime del host.&lt;/li&gt;
&lt;li&gt;El registro público entra en el flujo como fuente de discovery, nunca como allowlist automática.&lt;/li&gt;
&lt;li&gt;La allowlist interna fija versión, fuente, owner, datos, scopes, transporte, fecha de revisión y expiración.&lt;/li&gt;
&lt;li&gt;Los paquetes locales se fijan, escanean y ejecutan con sandbox, red, filesystem y credenciales mínimos.&lt;/li&gt;
&lt;li&gt;Las definiciones de tools se inspeccionan y se vuelven a aprobar si cambian.&lt;/li&gt;
&lt;li&gt;Las acciones sensibles muestran parámetros completos y requieren consentimiento o aprobación humana.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;MCP Registry es una pieza necesaria para que el ecosistema deje de repartir fragmentos de configuración por README y capturas. Estandariza discovery y metadata, y permite que clientes y empresas hablen el mismo idioma de catálogo. Eso es valioso, pero no es una auditoría de seguridad.&lt;/p&gt;

&lt;p&gt;Publica como maintainer con releases reproducibles, versiones inmutables y CI; consume como equipo con una allowlist, artefactos fijados, sandbox y reevaluación por cambios. Si conviertes un registry en «instalar lo que aparezca», acabas de automatizar la parte peligrosa de tu supply chain. Si lo usas para hacer explícitas procedencia y políticas, reduces fricción sin regalar privilegios.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es MCP Registry?
&lt;/h3&gt;

&lt;p&gt;Es un estándar y catálogo de metadata para descubrir servidores Model Context Protocol. El Official MCP Registry ofrece una API pública para que clientes y sub-registros consulten fichas de servidores.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿El MCP Registry oficial certifica que un servidor sea seguro?
&lt;/h3&gt;

&lt;p&gt;No. Ayuda a descubrir metadata y comprobar propiedad de namespaces, pero no sustituye revisión de código, integridad del artefacto, permisos mínimos, sandbox ni controles de ejecución.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué contiene server.json?
&lt;/h3&gt;

&lt;p&gt;Describe el nombre, versión, repositorio y cómo obtener o conectar el servidor, por ejemplo un paquete con transporte STDIO o un endpoint remoto. No debe almacenar secretos runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo cambiar un servidor ya publicado?
&lt;/h3&gt;

&lt;p&gt;No se reescribe esa versión. Publica una versión nueva de &lt;code&gt;server.json&lt;/code&gt;; las versiones publicadas son inmutables y deben ser únicas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Necesito un registro privado para mi empresa?
&lt;/h3&gt;

&lt;p&gt;Si quieres publicar servicios internos o aplicar una allowlist de servidores aprobados, sí. Puedes usar una implementación compatible v0.1 o un catálogo interno que fije versiones, owners, permisos y caducidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo instalar automáticamente los resultados del registry?
&lt;/h3&gt;

&lt;p&gt;No. Úsalo para discovery y somete cada versión a política: publisher, paquete o endpoint, dependencias, tools, scopes, sandbox y aprobación antes de habilitarla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo publicar y gobernar un servidor con MCP Registry
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Definir el límite.&lt;/strong&gt; Enumera tools, datos, efectos y permisos; elimina capacidades que no pertenecen al primer release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publicar el artefacto.&lt;/strong&gt; Compila, prueba y publica el paquete o endpoint antes de crear la ficha de registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vincular la procedencia.&lt;/strong&gt; Elige namespace, configura verificación GitHub, DNS o HTTP y limita quién puede publicar desde CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generar server.json.&lt;/strong&gt; Usa &lt;code&gt;mcp-publisher init&lt;/code&gt;, declara versión exacta, repositorio y transporte sin incluir secretos runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validar en CI.&lt;/strong&gt; Comprueba schema, coherencia con package metadata y un arranque real que complete &lt;code&gt;initialize&lt;/code&gt; en sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publicar una versión.&lt;/strong&gt; Autentica el publisher con identidad de CI y registra la versión única; no reescribas releases publicados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verificar discovery.&lt;/strong&gt; Consulta el detalle de esa versión en la API y guarda la respuesta revisada como evidencia de release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crear allowlist.&lt;/strong&gt; Fija versión, fuente, owner, clasificación de datos, scopes, transporte, revisión y fecha de expiración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aislar ejecución.&lt;/strong&gt; Usa credenciales por servidor, filesystem y red mínimos, y aprobación humana para acciones sensibles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reevaluar cambios.&lt;/strong&gt; Altera paquete, endpoint, tool, schema o scope y obliga una revisión antes de avanzar a la nueva versión.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/registry/about" rel="noopener noreferrer"&gt;MCP Registry: about&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/registry/quickstart" rel="noopener noreferrer"&gt;MCP Registry: quickstart para publicar un servidor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/registry/versioning" rel="noopener noreferrer"&gt;MCP Registry: versionado de servidores publicados&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/registry/faq" rel="noopener noreferrer"&gt;MCP Registry: FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/official-registry-api.md" rel="noopener noreferrer"&gt;Official MCP Registry API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;Model Context Protocol: Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/administer-copilot/manage-mcp-usage/configure-mcp-registry" rel="noopener noreferrer"&gt;GitHub Docs: configurar un MCP registry empresarial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP MCP Security Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad, permisos y supply chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/oauth-21-mcp-servidores-remotos/" rel="noopener noreferrer"&gt;OAuth 2.1 para servidores MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-apps-ui-interactiva-agentes/" rel="noopener noreferrer"&gt;MCP Apps: UI interactiva para tools MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/docker-mcp-toolkit-agentes-locales/" rel="noopener noreferrer"&gt;Docker MCP Toolkit: agentes locales y seguridad&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>n8n y agentes de IA: cómo crear workflows fiables con aprobación humana</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Wed, 12 Aug 2026 09:08:57 +0000</pubDate>
      <link>https://dev.to/khavel/n8n-y-agentes-de-ia-como-crear-workflows-fiables-con-aprobacion-humana-4hea</link>
      <guid>https://dev.to/khavel/n8n-y-agentes-de-ia-como-crear-workflows-fiables-con-aprobacion-humana-4hea</guid>
      <description>&lt;p&gt;n8n puede convertir un agente en un workflow operativo, pero el canvas no sustituye permisos, contratos ni evaluación. Diseña una tarea estrecha, separa decisiones de efectos y deja al humano la última palabra cuando haya riesgo.&lt;/p&gt;

&lt;p&gt;Un agente de IA en n8n es un workflow en el que un modelo elige una o varias tools para alcanzar una meta. Es útil cuando el trabajo tiene pasos, integraciones y decisiones que cambian; no es una excusa para dar acceso indiscriminado a correo, bases de datos o producción.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;n8n agentes IA&lt;/code&gt;. La intención es práctica: cómo diseñar y operar un primer workflow agentic que use datos y herramientas reales sin convertir una demo visual en una automatización opaca.&lt;/p&gt;

&lt;p&gt;Mi postura: usa n8n para orquestar decisiones acotadas y efectos revisables. Si el agente envía un email, abre un ticket, cambia un registro o llama a una API con coste, el workflow debe tener validación, una aprobación cuando el impacto lo justifique y evidencia de qué ocurrió.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Qué es un agente de IA en n8n — y qué no es
&lt;/h3&gt;

&lt;p&gt;Una cadena ejecuta pasos que tú defines de antemano: recibir un formulario, normalizar campos, llamar una API y guardar una respuesta. Un agente añade una decisión del modelo sobre qué tool usar, con qué argumentos y en qué orden. Esa flexibilidad tiene valor cuando la entrada es ambigua; también introduce rutas de fallo que un workflow determinista no tenía.&lt;/p&gt;

&lt;p&gt;No confundas un AI Agent con cualquier nodo que llame a un LLM. Para clasificación, extracción con schema, resumen o transformación de texto, una cadena con salida estructurada suele ser más barata, fácil de probar y más segura. El agente entra cuando necesita seleccionar capacidades y la selección no cabe razonablemente en un &lt;code&gt;if&lt;/code&gt; explícito.&lt;/p&gt;

&lt;p&gt;La prueba de realidad es sencilla: describe la tarea sin mencionar el modelo. Si no puedes enumerar input permitido, resultado esperado, herramientas necesarias, dueño de la decisión y efecto externo, todavía no tienes un caso de uso; tienes una intención vaga.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxeac23u4ndj9bqbcrowx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxeac23u4ndj9bqbcrowx.png" alt="Diagrama de workflow agentic con evento de entrada, validación, agente con herramientas limitadas, aprobación humana, cola de trabajos, ruta de error y registro de auditoría" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El modelo puede proponer una acción y elegir una tool; el workflow conserva la autoridad sobre validación, aprobación, reintentos y auditoría.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  La arquitectura mínima: separar pensar, validar y actuar
&lt;/h3&gt;

&lt;p&gt;Un flujo que desplegaría tiene seis límites. Un trigger recibe el evento; una capa de normalización reduce el input a campos permitidos; el agente razona solo con contexto necesario; las tools tienen contratos pequeños; una puerta de aprobación detiene efectos sensibles; y una capa de auditoría deja evidencia de cada decisión. La cola y el error handler sostienen el proceso si hay trabajo largo o fallos transitorios.&lt;/p&gt;

&lt;p&gt;El error común es conectar Gmail, Slack, CRM, GitHub y una base de datos como tools desde el primer día. El modelo ya no ve cinco integraciones: ve cinco superficies de acción con credenciales y consecuencias distintas. Empieza con una única tool de lectura y añade otra solo cuando tengas un caso, una autorización y una métrica que la justifiquen.&lt;/p&gt;

&lt;p&gt;El prompt no es esa frontera. El prompt explica la tarea; el nodo, credencial, schema y política de aprobación imponen lo que puede suceder. Si un prompt dice «no borres nada» pero una tool permite borrar y no exige aprobación, tu sistema depende de que el modelo obedezca siempre. Eso no es un control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Caso de inicio que sí merece un agente
&lt;/h2&gt;

&lt;p&gt;Un buen primer caso es el triage de incidencias internas. Un webhook recibe título, descripción, servicio y enlace; el agente consulta una base de conocimiento de solo lectura y propone categoría, prioridad, evidencia y siguiente acción. El workflow valida el objeto de salida. Solo después, una persona aprueba crear o actualizar el ticket en el sistema correspondiente.&lt;/p&gt;

&lt;p&gt;Ese diseño ofrece un baseline claro. Puedes medir si la categoría es correcta, si la evidencia existe, si eligió una tool adecuada y cuánto tarda. También puedes comparar una cadena simple contra el agente: si la cadena resuelve el 90% de los casos sin tools, probablemente no necesitas más autonomía para ese 90%.&lt;/p&gt;

&lt;p&gt;Evita empezar por «responde tickets automáticamente». Es una mezcla de clasificación, conocimiento, identidad, tono, SLA y acción externa. Descompón esa frase en etapas; automatiza primero la que sea reversible y tenga un criterio de aceptación objetivo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contratos: el agente propone JSON; el workflow decide
&lt;/h2&gt;

&lt;p&gt;Haz que el agente devuelva una decisión pequeña y validable, no párrafos que otro nodo tenga que interpretar. Un contrato útil incluye &lt;code&gt;category&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;evidence&lt;/code&gt;, &lt;code&gt;next_action&lt;/code&gt; y &lt;code&gt;requires_approval&lt;/code&gt;. Mantén los enums limitados y exige que la evidencia proceda del input o de una tool consultada; no conviertas una confianza inventada en una orden operativa.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Ejemplo de contrato para un triage. En n8n puedes implementarlo con Structured Output Parser o con un nodo de validación posterior; lo importante es que la rama de escritura solo reciba objetos que pasen el schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;decision.schema.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next_action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"requires_approval"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"incident"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"next_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"request_context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft_ticket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requires_approval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un schema no hace verdadera la respuesta; solo evita que un formato ambiguo avance. La regla es: validation failure significa detener, pedir más contexto o enviar a humano. Nunca significa adivinar campos que faltan y continuar con la escritura.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools estrechas y credenciales con alcance mínimo
&lt;/h3&gt;

&lt;p&gt;Una tool debe corresponder a una operación que puedas describir como una API segura. &lt;code&gt;buscar_runbook(servicio, consulta)&lt;/code&gt; es mejor que «acceso a toda la wiki». &lt;code&gt;crear_borrador_ticket(titulo, cuerpo, prioridad)&lt;/code&gt; es mejor que «administrar el proyecto». Menos parámetros, resultados limitados, límites de tamaño y un owner claro hacen que la tool sea más fácil de evaluar y revocar.&lt;/p&gt;

&lt;p&gt;Usa credenciales separadas por entorno y workflow. Compartir un workflow puede permitir a sus editores usar las credenciales que contiene; por eso, antes de compartir, revisa quién necesita editarlo y qué identidad ejecuta cada nodo. Un canvas compartido no es una razón para usar una cuenta de administrador global.&lt;/p&gt;

&lt;p&gt;Trata cualquier documento, email, issue o resultado de búsqueda que entre al contexto como datos no confiables. Puede contener instrucciones para el modelo. Filtra qué campos se exponen a la tool, separa los datos de las instrucciones de sistema y nunca dejes que texto recuperado cambie scopes, URLs sensibles o identificadores de tenant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aprobación humana: dónde debe detenerse el flujo
&lt;/h3&gt;

&lt;p&gt;La revisión humana es útil antes de una mutación, no después de descubrir una mutación. En n8n, una operación puede pausar y pedir aprobación; para procesos más complejos, usa una espera y una interfaz o canal de decisión que conserve el &lt;code&gt;execution_id&lt;/code&gt;, actor, payload propuesto y fecha de expiración.&lt;/p&gt;

&lt;p&gt;La aprobación debería mostrar lo que una persona necesita para responsabilizarse: acción propuesta, campos concretos, evidencia usada, destino, coste potencial y enlace a la ejecución. «El agente recomienda continuar» no es una solicitud de aprobación; es una transferencia opaca de responsabilidad.&lt;/p&gt;

&lt;p&gt;Define políticas por impacto. Lectura de documentación pública puede seguir sin gate. Crear un borrador puede requerir revisión por muestreo. Enviar un correo, borrar, desplegar, cambiar permisos o tocar datos de cliente debe requerir aprobación explícita y registrar quién la otorgó. Si no puedes esperar, probablemente la acción no debería depender de un agente libre.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  MCP en n8n: útil, pero no un catálogo sin freno
&lt;/h3&gt;

&lt;p&gt;MCP puede servir para exponer herramientas externas al agente o para publicar workflows seleccionados como capacidades. El patrón conserva las mismas reglas: tools descubiertas no son tools aprobadas. Define un allowlist por workflow, usa identidades y scopes mínimos y registra qué servidor MCP y método ejecutó la operación.&lt;/p&gt;

&lt;p&gt;No conectes un servidor MCP remoto solo porque ofrece muchas herramientas. Revisa su autorización, transporte, procedencia, datos que recibe y acciones posibles. Si el servidor toca recursos internos, aplica OAuth, audiencia y scopes en el servidor, como explicamos en la guía de OAuth para MCP; n8n no convierte una credencial amplia en una política segura.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mi criterio: introduce MCP después de validar una tool nativa o HTTP estrecha. Si no sabes qué tool del catálogo necesita el caso, no es momento de dar al agente decenas de opciones. La selección de capabilities también necesita un diseño de producto y de seguridad.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Escalado: una ejecución no es una arquitectura
&lt;/h3&gt;

&lt;p&gt;Un workflow corto puede vivir en una instancia. Si tienes trabajos asíncronos, picos, reintentos o aprobaciones que duran horas, separa la recepción del evento del trabajo. n8n documenta queue mode con procesos main, workers y un broker; úsalo cuando la carga lo justifique, no como decoración de un prototipo.&lt;/p&gt;

&lt;p&gt;Pon idempotencia en el borde: conserva un ID de evento y evita que un retry cree dos tickets o envíe dos mensajes. El modelo puede reintentarse; una acción mutante no debería duplicarse sin una clave de negocio o un check de estado. En la rama de error, clasifica fallo de proveedor, timeout, validación, permiso y aprobación vencida; cada uno necesita una recuperación distinta.&lt;/p&gt;

&lt;p&gt;Mide cola, duración p50/p95, tasa de reintento, acciones propuestas, aprobadas y rechazadas, tools llamadas y coste por workflow. Un gráfico de ejecuciones exitosas sin conocer cuántas decisiones fueron correctas solo mide que el sistema hizo algo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Evaluación antes de activar el workflow
&lt;/h2&gt;

&lt;p&gt;n8n ofrece evaluaciones ligeras y métricas, pero tu dataset importa más que el botón. Crea al menos 30 casos con entradas normales, incompletas, ambiguas y hostiles. Para cada uno, guarda decisión esperada, tools permitidas, tool calls prohibidas, evidencia mínima y si el caso debe pedir aprobación.&lt;/p&gt;

&lt;p&gt;Define gates antes de modificar prompt, modelo o herramientas: porcentaje de categoría correcta, tasa de JSON válido, evidencia verificable, llamadas indebidas, falsos positivos de escritura, tiempo y coste. No cambies modelo y prompt a la vez si quieres saber por qué apareció una regresión.&lt;/p&gt;

&lt;p&gt;La evaluación de trayectorias es especialmente valiosa en agentes: la respuesta final puede parecer buena aunque haya consultado una fuente errónea, usado la herramienta equivocada o intentado escribir sin aprobación. Registra ruta y argumentos redactados, no solo el texto final.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seguridad y operaciones que no dejaría para después
&lt;/h2&gt;

&lt;p&gt;Ejecuta el security audit de n8n al incorporar un workflow sensible. Revisa credenciales sin uso, webhooks sin protección, nodos con acceso a filesystem o ejecución de comandos, community nodes y configuración de instancia. Es una señal de higiene, no la garantía de que un agente entiende tus permisos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Separa desarrollo, staging y producción. Prueba con identidades sandbox, datos sintéticos y destinos que no generen efectos reales. Mantén secretos en el gestor apropiado, redáctalos de logs y limita acceso al historial de ejecuciones: un prompt o respuesta puede incluir datos de cliente aunque la tool haya sido de solo lectura.&lt;/p&gt;

&lt;p&gt;Cuando el workflow evolucione, versiona export, prompt, schema, modelo, lista de tools y credenciales lógicas. Cualquier cambio en una de estas piezas es una versión candidata que debe pasar el dataset y un canary, no un ajuste inocente en el canvas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checklist para un primer agente n8n
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;La tarea tiene un input, una salida y un owner definidos; no es «automatizar soporte».&lt;/li&gt;
&lt;li&gt;Una cadena determinista no resuelve el caso igual de bien y más barata.&lt;/li&gt;
&lt;li&gt;El agente recibe solo contexto necesario y devuelve un objeto que pasa schema.&lt;/li&gt;
&lt;li&gt;Cada tool tiene propósito, allowlist, límite de datos, identidad y scope mínimos.&lt;/li&gt;
&lt;li&gt;Las mutaciones se separan de la decisión y pasan por aprobación cuando el impacto lo exige.&lt;/li&gt;
&lt;li&gt;Los retries son idempotentes y la rama de error distingue causas recuperables de denegaciones.&lt;/li&gt;
&lt;li&gt;Existe un dataset con casos normales, ambiguos y hostiles, más gates de calidad, trayectoria, latencia y coste.&lt;/li&gt;
&lt;li&gt;Hay trazas y auditoría con IDs, decisiones, tool calls y aprobaciones, sin almacenar secretos por defecto.&lt;/li&gt;
&lt;li&gt;Staging y producción usan credenciales distintas; el workflow no necesita una cuenta admin global.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;n8n es una buena capa de orquestación cuando quieres que una decisión de IA toque integraciones reales de forma visible. Su virtud no es dibujar nodos: es darte lugares claros donde validar, pausar, reintentar y auditar. Úsalos.&lt;/p&gt;

&lt;p&gt;Empieza con un triage de lectura, una tool y una salida estructurada. Añade aprobación antes de la primera escritura. Mide la trayectoria antes de celebrar la respuesta. Cuando esas tres cosas sean aburridas y repetibles, entonces tendrás base para automatizar más; antes, solo tendrás una demo con acceso a sistemas reales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es un agente de IA en n8n?
&lt;/h3&gt;

&lt;p&gt;Es un workflow donde un modelo puede decidir qué herramientas usar para una meta. A diferencia de una cadena fija, la ruta puede variar según la entrada, por lo que necesita más control y evaluación.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo conviene usar n8n Agent y cuándo una cadena?
&lt;/h3&gt;

&lt;p&gt;Usa una cadena para extracción, clasificación, resumen y pasos definidos. Usa un agente cuando la selección de una tool o el orden de pasos dependa de la situación y puedas limitar y auditar esa decisión.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puede un agente n8n enviar emails o modificar un CRM?
&lt;/h3&gt;

&lt;p&gt;Técnicamente sí, pero no debería hacerlo sin una identidad mínima, una tool estrecha, validación de argumentos e idealmente aprobación humana para efectos externos o irreversibles.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo pruebo un agente antes de producción?
&lt;/h3&gt;

&lt;p&gt;Crea un dataset de casos normales, ambiguos y hostiles; mide decisión, formato, evidencia, tools llamadas, acciones indebidas, latencia y coste. Ejecuta un canary con credenciales y destinos sandbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿MCP hace seguro un workflow de n8n?
&lt;/h3&gt;

&lt;p&gt;No. MCP conecta capacidades; aún debes revisar servidor, autenticación, scopes, tools expuestas, datos y aprobaciones. Una tool remota con permisos amplios sigue siendo un riesgo amplio.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Necesito queue mode para un agente?
&lt;/h3&gt;

&lt;p&gt;No para un piloto pequeño. Sí puede ser necesario cuando hay carga, trabajos asíncronos, aprobaciones largas o necesidad de separar la recepción de eventos del procesamiento por workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo desplegar un primer agente de IA en n8n
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acotar la misión.&lt;/strong&gt; Elige una tarea reversible y de lectura, como clasificar y proponer el triage de una incidencia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definir el contrato.&lt;/strong&gt; Especifica campos de entrada, schema de decisión, evidencia requerida, herramientas permitidas y acciones prohibidas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crear una tool mínima.&lt;/strong&gt; Conecta una única fuente de conocimiento de solo lectura y limita datos, credencial y parámetros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Añadir validación.&lt;/strong&gt; Rechaza salida que no pase schema o que no contenga evidencia suficiente; no inventes defaults para continuar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separar escritura.&lt;/strong&gt; Encierra crear ticket, enviar mensaje o cambiar registro en una rama posterior con clave idempotente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configurar aprobación.&lt;/strong&gt; Muestra acción, payload, destino, evidencia y ejecución a una persona antes de la primera mutación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crear dataset.&lt;/strong&gt; Incluye casos normales, ambiguos, incompletos y hostiles con decisión y trayectoria esperadas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir y canary.&lt;/strong&gt; Compara calidad, tools, latencia, coste y aprobaciones en staging antes de abrir una parte pequeña de tráfico.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditar y versionar.&lt;/strong&gt; Guarda versión de workflow, prompt, schema, modelo y allowlist; redacta secretos de ejecuciones y trazas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/advanced-ai/examples/understand-agents/" rel="noopener noreferrer"&gt;n8n Docs: What is an agent?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/advanced-ai/intro-tutorial/" rel="noopener noreferrer"&gt;n8n Docs: Build an AI workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/advanced-ai/examples/human-fallback/" rel="noopener noreferrer"&gt;n8n Docs: Human fallback for AI workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/advanced-ai/evaluations/overview/" rel="noopener noreferrer"&gt;n8n Docs: Evaluations overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/hosting/scaling/queue-mode/" rel="noopener noreferrer"&gt;n8n Docs: Queue mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/hosting/securing/security-audit/" rel="noopener noreferrer"&gt;n8n Docs: Security audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/workflows/sharing/" rel="noopener noreferrer"&gt;n8n Docs: workflow sharing and credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/n8n-io/n8n" rel="noopener noreferrer"&gt;n8n source repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad, permisos y supply chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/evaluacion-rag-produccion-metricas-datasets/" rel="noopener noreferrer"&gt;Evaluación RAG en producción&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/oauth-21-mcp-servidores-remotos/" rel="noopener noreferrer"&gt;OAuth 2.1 para servidores MCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>Imagen 4 shuts down Monday, and Google's own docs disagree about where to go</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:50:37 +0000</pubDate>
      <link>https://dev.to/khavel/imagen-4-shuts-down-monday-and-googles-own-docs-disagree-about-where-to-go-5aid</link>
      <guid>https://dev.to/khavel/imagen-4-shuts-down-monday-and-googles-own-docs-disagree-about-where-to-go-5aid</guid>
      <description>&lt;p&gt;Model-lifecycle noise is almost always about chat models. Meanwhile Google's &lt;em&gt;generative media&lt;/em&gt; line — image, video, music — has been quietly running the same deprecation machinery for a year, and one of its deadlines lands on &lt;strong&gt;Monday, August 17, 2026&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every number and quote below was read this morning off Google's own pages, not off a tracker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ai.google.dev/gemini-api/docs/pricing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ai.google.dev/gemini-api/docs/deprecations&lt;/code&gt; (page footer: &lt;em&gt;Last updated 2026-08-03 UTC&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The deadline
&lt;/h2&gt;

&lt;p&gt;Three model ids go dark on August 17:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;imagen-4.0-generate-001
imagen-4.0-ultra-generate-001
imagen-4.0-fast-generate-001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They were released June 24, 2025. That's it — a fourteen-month production life for what the pricing page still describes as &lt;em&gt;"Our latest image generation model, with significantly better text rendering and better overall image quality."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next has two official answers
&lt;/h2&gt;

&lt;p&gt;The pricing page carries this warning, verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Warning: Imagen 4 models (&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;, &lt;code&gt;imagen-4.0-ultra-generate-001&lt;/code&gt;, &lt;code&gt;imagen-4.0-fast-generate-001&lt;/code&gt;) are deprecated and will be shut down on August 17, 2026; migrate to &lt;strong&gt;Gemini 2.5 Flash Image&lt;/strong&gt; to avoid service disruption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The deprecations table, for the same three ids, puts something else in its &lt;code&gt;Recommended replacement&lt;/code&gt; column:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Release date&lt;/th&gt;
&lt;th&gt;Shutdown date&lt;/th&gt;
&lt;th&gt;Recommended replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;June 24, 2025&lt;/td&gt;
&lt;td&gt;August 17, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-ultra-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;June 24, 2025&lt;/td&gt;
&lt;td&gt;August 17, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-fast-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;June 24, 2025&lt;/td&gt;
&lt;td&gt;August 17, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two first-party surfaces, two different destinations. And it gets worse if you follow the banner, because &lt;strong&gt;Gemini 2.5 Flash Image is itself deprecated&lt;/strong&gt; — it's in the same table with a shutdown of &lt;strong&gt;October 2, 2026&lt;/strong&gt;, six weeks after the model it's being offered as an escape from.&lt;/p&gt;

&lt;p&gt;Follow the table's pointer for &lt;em&gt;that&lt;/em&gt; model and you get this chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini-2.5-flash-image            shutdown Oct 2, 2026
  └─ recommended: gemini-3.1-flash-image-preview
                                  shutdown Jun 25, 2026   ← already passed
       └─ recommended: gemini-3.1-flash-image
                                  no shutdown date announced
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middle hop is a model whose own shutdown date was 47 days ago. Nothing here is malicious — it's an ordinary docs-consistency problem across three tables that each move on their own schedule. But if you're writing the migration, you need to know that the pointer requires two hops and that one of the hops is dead.&lt;/p&gt;

&lt;p&gt;Worth noting what the pricing page does &lt;em&gt;not&lt;/em&gt; say: in the Gemini 2.5 Flash Image section there is no deprecation warning at all. It reads as a normal, buyable model. A reader who follows the banner lands on a page that looks clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  It isn't just an id swap — the billing unit changes
&lt;/h2&gt;

&lt;p&gt;Imagen 4 bills &lt;strong&gt;per image&lt;/strong&gt;, flat, paid tier only (there is no free tier):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Price per image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Imagen 4 Fast&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Imagen 4 Standard&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Imagen 4 Ultra&lt;/td&gt;
&lt;td&gt;$0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The banner's target, Gemini 2.5 Flash Image, also quotes a flat per-image output price: &lt;strong&gt;$0.039 per image&lt;/strong&gt; on the Standard tier.&lt;/p&gt;

&lt;p&gt;The table's target, Gemini 3.1 Flash Image, bills &lt;strong&gt;per token&lt;/strong&gt; — image output at $60.00 per 1M tokens — which Google converts on the page to a &lt;em&gt;resolution-tiered&lt;/em&gt; rate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Price per image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.5K&lt;/td&gt;
&lt;td&gt;$0.045&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1K&lt;/td&gt;
&lt;td&gt;$0.067&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2K&lt;/td&gt;
&lt;td&gt;$0.101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K&lt;/td&gt;
&lt;td&gt;$0.151&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So &lt;code&gt;imagen-4.0-generate-001&lt;/code&gt; at $0.04 costs the same whatever you render; its replacement costs $0.045 at 0.5K and &lt;strong&gt;$0.151 at 4K&lt;/strong&gt; — a shade under 3.8x — and image size becomes a line item in your cost model where it previously wasn't one. That's the part that breaks a spreadsheet, not the model id.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Google shutdown date is a floor, not a schedule
&lt;/h2&gt;

&lt;p&gt;This is the bit worth internalizing, and Google states it plainly on the deprecations page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deprecation announcements are made on the Release notes page, and the announced &lt;strong&gt;earliest shutdown dates&lt;/strong&gt; are tracked on this page. Already-shutdown models are indicated with gray backgrounds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now look at the Veo rows. &lt;code&gt;veo-3.0-generate-001&lt;/code&gt;, &lt;code&gt;veo-3.0-fast-generate-001&lt;/code&gt; and &lt;code&gt;veo-2.0-generate-001&lt;/code&gt; all carry a shutdown date of &lt;strong&gt;June 30, 2026&lt;/strong&gt; — 42 days ago as I write this. They are &lt;strong&gt;not&lt;/strong&gt; gray. The pricing page still sells them, with live per-second rates and a warning in the future tense: &lt;em&gt;"will be shut down on June 30, 2026."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the date is a lower bound on when the endpoint disappears, not a prediction of when it will. Both failure modes are real: build a migration that assumes the date holds and you may cut over needlessly; build one that assumes it slips and you find out the hard way. The only safe read is "not before this date."&lt;/p&gt;

&lt;h2&gt;
  
  
  The stable video model is the deprecated one
&lt;/h2&gt;

&lt;p&gt;Also from the pricing page, about Veo 3 — the deprecated line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our &lt;strong&gt;stable&lt;/strong&gt; video generation model, available to developers on the paid tier of the Gemini API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And about Veo 3.1, the model the deprecations table names as its replacement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Preview models may change before becoming stable&lt;/strong&gt; and have more restrictive rate limits.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every Veo model in the Gemini API is now either deprecated or preview. The table hedges accordingly — the replacement cell reads &lt;code&gt;veo-3.1-generate-preview&lt;/code&gt; &lt;em&gt;"or the GA models on the Gemini Enterprise Agent Platform"&lt;/em&gt;, which is a different product with a different surface. If you need a generally-available video model inside the Gemini API specifically, as of today the pricing page does not offer you one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google's generative-media lifecycle, in one table
&lt;/h2&gt;

&lt;p&gt;Every model in Google's Imagen, Veo, Lyria and image-generation sections that carries a shutdown date, oldest deadline first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Shutdown&lt;/th&gt;
&lt;th&gt;Recommended replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-3.0-generate-002&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nov 10, 2025&lt;/td&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veo-3.0-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nov 12, 2025&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veo-3.1-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veo-3.0-fast-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nov 12, 2025&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veo-3.1-fast-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.0-flash-preview-image-generation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nov 14, 2025&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jan 15, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-generate-preview-06-06&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feb 17, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-ultra-generate-preview-06-06&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Feb 17, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;imagen-4.0-ultra-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jun 25, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3-pro-image-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jun 25, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3-pro-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veo-3.0-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jun 30, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veo-3.1-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veo-3.0-fast-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jun 30, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veo-3.1-fast-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;veo-2.0-generate-001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jun 30, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;veo-3.1-generate-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aug 17, 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;imagen-4.0-ultra-generate-001&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aug 17, 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;imagen-4.0-fast-generate-001&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aug 17, 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash-image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oct 2, 2026&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash-image-preview&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The other models in those same sections read &lt;em&gt;"No shutdown date announced"&lt;/em&gt;: &lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;, &lt;code&gt;gemini-3-pro-image&lt;/code&gt;, &lt;code&gt;veo-3.1-generate-preview&lt;/code&gt;, &lt;code&gt;veo-3.1-fast-generate-preview&lt;/code&gt;, &lt;code&gt;veo-3.1-lite-generate-preview&lt;/code&gt;, &lt;code&gt;lyria-3-clip-preview&lt;/code&gt;, &lt;code&gt;lyria-3-pro-preview&lt;/code&gt;, &lt;code&gt;lyria-realtime-exp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two patterns fall out of the column of dates. Preview lifetimes are short and getting shorter — &lt;code&gt;gemini-3.1-flash-image-preview&lt;/code&gt; ran from Feb 26 to Jun 25, 2026, about four months, where the Imagen 4 previews got roughly eight. And a lineage gets re-pointed as it goes: Imagen 3 pointed at Imagen 4, which now points out of Imagen entirely and into the Gemini image line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you probably haven't seen this tracked
&lt;/h2&gt;

&lt;p&gt;Because these models don't fit the schema everyone uses. They have no context window and no per-token price — they bill per image or per second of video — so a per-token price tracker either drops them or shows a row of blanks.&lt;/p&gt;

&lt;p&gt;That includes ours, partly. &lt;a href="https://aimodelwatch.dev" rel="noopener noreferrer"&gt;AI Model Watch&lt;/a&gt; carries all 25 of Google's generative-media models with their lifecycle fields, but our price fields are per-token, so on these rows they're &lt;code&gt;null&lt;/code&gt; and the real rate ($0.04 per image, $0.40 per second) lives in the row's &lt;code&gt;notes&lt;/code&gt; with the source URL it was read from. A &lt;code&gt;null&lt;/code&gt; there means "this model isn't priced in tokens," not "we couldn't find it" — but it's a genuine limitation of the schema and worth saying out loud rather than papering over.&lt;/p&gt;

&lt;p&gt;The lifecycle half is structured and free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://aimodelwatch.dev/api/models.json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.models[]
      | select(.provider=="Google")
      | select(.modality | any(test("image-out|video-out|audio-out")))
      | [(.retires_on // "none"), .id, .status, (.replacement // "-")] | @tsv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns all 25 rows, dated deadlines first. (There's also &lt;code&gt;/api/deprecations.json&lt;/code&gt;, which is the lifecycle-only projection — it's narrower, and it has no &lt;code&gt;modality&lt;/code&gt; field, so filter that one on &lt;code&gt;.provider&lt;/code&gt; and &lt;code&gt;.id&lt;/code&gt; instead.)&lt;/p&gt;

&lt;p&gt;No key, no signup, CORS open, and every row carries the &lt;code&gt;source_url&lt;/code&gt; it was read from so you can check it against Google yourself. Which you should — that's the whole point of the &lt;code&gt;source_url&lt;/code&gt; being there.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're on Imagen 4 right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You have until &lt;strong&gt;Monday, August 17&lt;/strong&gt;. Test against the &lt;em&gt;table's&lt;/em&gt; target, &lt;code&gt;gemini-3.1-flash-image&lt;/code&gt;, not the banner's — the banner's target has an October date on it.&lt;/li&gt;
&lt;li&gt;Re-price before you cut over. Flat per-image becomes resolution-tiered per-token, and 4K lands near 3.8x what Imagen 4 Standard cost you.&lt;/li&gt;
&lt;li&gt;If you're on Veo instead, you have more time than the page implies, and less certainty. June 30 came and went; nothing is gray yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The general version, which is the reason I keep a catalog of this at all: a model shutdown is a date on someone else's calendar, and nothing in your build system is watching it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>googlecloud</category>
      <category>api</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>OAuth 2.1 para MCP: cómo proteger servidores remotos sin romper los clientes</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:20:19 +0000</pubDate>
      <link>https://dev.to/khavel/oauth-21-para-mcp-como-proteger-servidores-remotos-sin-romper-los-clientes-4o2c</link>
      <guid>https://dev.to/khavel/oauth-21-para-mcp-como-proteger-servidores-remotos-sin-romper-los-clientes-4o2c</guid>
      <description>&lt;p&gt;Un servidor MCP remoto que lee documentos o ejecuta tools no puede confiar en que el cliente sea conocido. OAuth 2.1 no es un botón de login: es el contrato que descubre identidad, limita scopes y permite validar cada llamada sin convertir la autorización en un prompt.&lt;/p&gt;

&lt;p&gt;OAuth 2.1 para MCP es el mecanismo para que un cliente obtenga un token con permiso limitado y un servidor MCP remoto compruebe ese token antes de exponer tools, recursos o acciones. En MCP, el servidor protegido es un resource server; el host del agente es el OAuth client; y tu proveedor de identidad emite los access tokens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;OAuth 2.1 MCP&lt;/code&gt;. La intención es de implementación: publicar Protected Resource Metadata, descubrir el authorization server, usar Authorization Code + PKCE, definir scopes pequeños y validar issuer, audience, expiración y permisos en cada tool call.&lt;/p&gt;

&lt;p&gt;Mi postura: si tu MCP remoto puede tocar correo, documentos, datos de cliente o sistemas internos, no intentes resolver identidad con una API key compartida en una variable de entorno. Una key solo identifica una integración; no expresa quién pidió una acción ni qué alcance tenía. OAuth te da un contrato, pero sigues necesitando autorización de negocio en tu backend.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qué cambió y por qué importa ahora
&lt;/h3&gt;

&lt;p&gt;La revisión MCP 2026-07-28 endurece la autorización y elimina parte de la complejidad de sesiones del protocolo. Para OAuth, el cambio relevante es práctico: la especificación prioriza Client ID Metadata Documents (CIMD) para clientes que no tienen una relación previa con el servidor, deja Dynamic Client Registration como compatibilidad y exige discovery interoperable de metadata OAuth u OpenID Connect.&lt;/p&gt;

&lt;p&gt;No conviertas eso en una migración cosmética. Un servidor MCP público no puede suponer que conoce de antemano todos los hosts que se conectarán. El flujo debe permitir discovery sin aceptar redirect URIs arbitrarias, tokens para otra audiencia o scopes enormes porque son más cómodos de configurar.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La propiedad evergreen es que el patrón no depende de un host concreto. Cambiarán SDKs y pantallas de consentimiento; seguirán siendo necesarios un resource identifier estable, metadata verificable, PKCE, token validation y una política de autorización que no viva dentro del modelo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftoy5mkhhc0bw9uu6o0sq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftoy5mkhhc0bw9uu6o0sq.png" alt="Diagrama de OAuth para MCP con cliente, servidor MCP protegido, proveedor de identidad y sistema de datos; el flujo muestra challenge, metadata, consentimiento, token, validación de permisos y auditoría" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El servidor MCP valida el token antes de ejecutar una tool; el proveedor de identidad autentica y emite credenciales, pero no sustituye la política por tenant o recurso de tu aplicación.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  El mapa mental correcto: cuatro roles, dos decisiones
&lt;/h3&gt;

&lt;p&gt;Hay cuatro piezas. El cliente MCP (un host de agente) inicia la conexión y representa al usuario. El servidor MCP es el resource server que protege su endpoint HTTP. El authorization server autentica y emite tokens. Por último, tu API, base de datos o SaaS aguas abajo contiene el recurso real. No confundas el servidor MCP con el identity provider: uno recibe la tool call; el otro decide cómo se obtiene la identidad.&lt;/p&gt;

&lt;p&gt;OAuth resuelve la primera decisión: ¿este cliente presenta un token válido para este recurso y con estos scopes? Tu aplicación resuelve la segunda: ¿este usuario de este tenant puede leer este documento o ejecutar esta acción ahora? El &lt;code&gt;scope&lt;/code&gt; abre una capacidad general; la autorización de negocio revisa IDs, ownership, rol, estado y consecuencias.&lt;/p&gt;

&lt;p&gt;Por ejemplo, &lt;code&gt;tickets:read&lt;/code&gt; no autoriza a leer cualquier ticket. Autoriza a intentar la tool de lectura. El handler debe cargar el usuario desde claims verificadas y aplicar el filtro de tenant antes de consultar. Si pasas el &lt;code&gt;user_id&lt;/code&gt; que propone el modelo como autoridad, has vuelto a delegar seguridad al prompt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Flujo OAuth 2.1 de un servidor MCP remoto
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;El cliente llama al endpoint MCP sin token o con token insuficiente. El servidor devuelve &lt;code&gt;401 Unauthorized&lt;/code&gt; y un &lt;code&gt;WWW-Authenticate: Bearer&lt;/code&gt; que apunta a su Protected Resource Metadata (PRM).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;El cliente descarga el documento PRM. Ahí descubre el &lt;code&gt;resource&lt;/code&gt; que debe aparecer como audiencia, el authorization server permitido y los scopes que el recurso entiende. Si el &lt;code&gt;resource&lt;/code&gt; del JSON no coincide exactamente con el recurso pedido, debe rechazarlo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;El cliente descubre los endpoints OAuth u OpenID Connect del issuer, registra su identidad por pre-registro o CIMD cuando esté disponible y abre Authorization Code con PKCE. PKCE evita que otro proceso intercepte y canjee el código de autorización.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tras consentimiento, el authorization server emite un access token dirigido a tu resource identifier. El cliente repite la llamada MCP con &lt;code&gt;Authorization: Bearer …&lt;/code&gt;. El servidor valida firma o introspección, &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, scopes y cualquier claim de tenant antes de despachar una tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cada tool ejecuta autorización propia, registra actor, cliente, tool, recurso y resultado, y devuelve un error de autorización seguro cuando corresponda. No guardes el access token en trazas, mensajes de error ni contenido de tool.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Protected Resource Metadata: la pieza que suele faltar
&lt;/h2&gt;

&lt;p&gt;Protected Resource Metadata es un JSON servido por el resource server, no por el proveedor de login. Según RFC 9728 y MCP, publica el identificador del recurso y los authorization servers autorizados. En una ruta MCP como &lt;code&gt;https://mcp.acme.test/remote&lt;/code&gt;, el cliente puede buscar &lt;code&gt;https://mcp.acme.test/.well-known/oauth-protected-resource/remote&lt;/code&gt; o seguir el &lt;code&gt;resource_metadata&lt;/code&gt; del challenge, que debe tener prioridad.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Evita meter aquí una lista fantasiosa de permisos. &lt;code&gt;scopes_supported&lt;/code&gt; documenta lo que el servidor puede pedir; el challenge de una request concreta puede exigir un conjunto más preciso y el cliente debe tratar ese challenge como autoridad para ese intento. Es una forma de pedir consentimiento incremental sin entregar &lt;code&gt;admin:*&lt;/code&gt; al primer clic.&lt;/p&gt;

&lt;p&gt;Un ejemplo mínimo y explícito podría ser el siguiente. Los nombres de scopes son tuyos: diseña verbos y dominios que alguien de seguridad pueda revisar, no copias de los nombres de tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;/.well-known/oauth-protected-resource/mcp&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.example.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://login.example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"issues:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"issues:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"deployments:read"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example engineering MCP"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementación Node: challenge, metadata y guard de token
&lt;/h2&gt;

&lt;p&gt;El SDK MCP puede encargarse del transporte y del registro de tools, pero el borde HTTP debe seguir devolver metadata y rechazar tokens inválidos antes de llegar al modelo o a los sistemas internos. Este ejemplo usa Express y &lt;code&gt;jose&lt;/code&gt; para mostrar el contrato; adapta los endpoints y claims a tu proveedor de identidad. En producción, cachea JWKS respetando sus cabeceras y mantén las URLs de issuer y audiencia en configuración revisada, no en input del usuario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;auth-boundary.mjs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwtVerify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mcp.example.com/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://login.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/jwks.json`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/oauth-protected-resource/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorization_servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;scopes_supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issues:read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issues:write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Bearer&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`Bearer resource_metadata="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/oauth-protected-resource/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;", scope="issues:read"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issues:read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenant_id&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mcpHttpHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No copies este fragmento sin decidir si tu access token es JWT, opaco o ambos. Un token opaco normalmente se valida por introspección contra el authorization server; un JWT se valida contra claves públicas confiables. En ambos casos, la audiencia debe ser el resource identifier de tu MCP, no el nombre genérico de tu producto.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Scopes, audiencia y acciones sensibles
&lt;/h3&gt;

&lt;p&gt;Empieza con scopes legibles y estrechos: &lt;code&gt;repo:read&lt;/code&gt;, &lt;code&gt;issues:read&lt;/code&gt;, &lt;code&gt;issues:write&lt;/code&gt;, &lt;code&gt;deployments:read&lt;/code&gt;. No concedas &lt;code&gt;tools:*&lt;/code&gt; si solo necesitas una consulta. Los scopes no deben depender de un prompt ni de los argumentos declarados por el LLM; se comprueban en el servidor antes de ejecutar la operación.&lt;/p&gt;

&lt;p&gt;Separa lectura de escritura. Para &lt;code&gt;issues:write&lt;/code&gt;, añade una aprobación explícita en el host o una confirmación en tu aplicación antes de mutar. Para acciones de alto impacto —borrar, desplegar, cambiar permisos, enviar comunicación externa— utiliza scopes específicos, un segundo control contextual y logs de auditoría. OAuth reduce blast radius; no vuelve segura una tool excesivamente poderosa.&lt;/p&gt;

&lt;p&gt;La claim &lt;code&gt;aud&lt;/code&gt; es tu defensa contra token replay entre APIs. Un token emitido para &lt;code&gt;https://api.example.com&lt;/code&gt; no debe servir para &lt;code&gt;https://mcp.example.com/mcp&lt;/code&gt; solo porque comparten issuer. Valida audiencia exacta, no &lt;code&gt;startsWith&lt;/code&gt;, no el hostname a ojo, y nunca aceptes una audiencia enviada por el cliente.&lt;/p&gt;
&lt;h3&gt;
  
  
  CIMD, pre-registro y por qué DCR ya no es la primera opción
&lt;/h3&gt;

&lt;p&gt;El registro de cliente responde a otra pregunta: ¿qué aplicación está pidiendo el token? Si controlas cliente y servidor, el pre-registro de client ID y redirect URIs es simple y robusto. Si esperas hosts desconocidos, MCP prioriza Client ID Metadata Documents: el client ID puede ser una URL HTTPS que publica metadata verificable del cliente.&lt;/p&gt;

&lt;p&gt;Dynamic Client Registration puede seguir existiendo por compatibilidad, pero no debería ser el camino que abra registros ilimitados y redirect URIs sin validación. La especificación actual lo coloca como fallback. Trata cada mecanismo como una superficie de seguridad: limita métodos de autenticación, exige URIs exactas y registra el client ID que obtuvo consentimiento.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No prometas soporte universal antes de probar hosts reales. Algunos clientes aún llegarán con capacidades antiguas. Publica claramente qué versiones, mecanismo de registro y scopes admites; ofrece el fallback mínimo sin bajar la validación del token por «compatibilidad».&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores que rompen autorización MCP
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Proteger solo la pantalla de consentimiento y dejar &lt;code&gt;/mcp&lt;/code&gt; sin validar &lt;code&gt;Authorization&lt;/code&gt; en cada request.&lt;/li&gt;
&lt;li&gt;Aceptar cualquier issuer que aparezca en un JWT o construir el JWKS URL con una claim no confiable.&lt;/li&gt;
&lt;li&gt;Comprobar firma y expiración, pero omitir audiencia, scopes, tenant y autorización del recurso concreto.&lt;/li&gt;
&lt;li&gt;Usar una API key global como identidad del usuario y registrar todas las acciones como si las hiciera el servidor.&lt;/li&gt;
&lt;li&gt;Devolver access tokens, authorization codes, cabeceras Bearer o datos de consentimiento en logs de trazas.&lt;/li&gt;
&lt;li&gt;Entregar &lt;code&gt;write&lt;/code&gt; al conectar el servidor aunque el usuario solo quiera explorar datos de lectura.&lt;/li&gt;
&lt;li&gt;Confiar en que el modelo no invocará una tool peligrosa si el system prompt dice que tenga cuidado.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Checklist de lanzamiento
&lt;/h3&gt;

&lt;p&gt;El endpoint MCP remoto rechaza sin token y expone &lt;code&gt;WWW-Authenticate&lt;/code&gt; con &lt;code&gt;resource_metadata&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La Protected Resource Metadata devuelve &lt;code&gt;resource&lt;/code&gt; exacto, authorization server permitido y scopes revisados.&lt;/p&gt;

&lt;p&gt;El cliente usa Authorization Code con PKCE y redirecciones registradas de forma exacta.&lt;/p&gt;

&lt;p&gt;El servidor valida issuer, firma o introspección, audiencia, expiración y scope por request.&lt;/p&gt;

&lt;p&gt;Cada tool aplica control de tenant, rol, propiedad y estado además del scope OAuth.&lt;/p&gt;

&lt;p&gt;Lectura y escritura tienen scopes distintos; las mutaciones de impacto tienen aprobación y auditoría.&lt;/p&gt;

&lt;p&gt;Tokens, códigos y cabeceras de autorización están redaccionados de logs, errores y trazas.&lt;/p&gt;

&lt;p&gt;Hay tests para token de otra audiencia, scope insuficiente, issuer falso, tenant cruzado y request repetida.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;La autorización MCP bien hecha no se nota cuando todo va bien: el host descubre la identidad necesaria, obtiene permiso mínimo y la tool funciona. Se nota cuando alguien conecta un cliente nuevo, intenta reutilizar un token contra otro recurso o pide una acción que no le corresponde; ahí el servidor debe fallar de forma predecible y auditable.&lt;/p&gt;

&lt;p&gt;Mi recomendación es empezar con un solo recurso remoto y dos scopes de lectura/escritura, no con un catálogo enorme de permisos. Publica PRM, valida &lt;code&gt;aud&lt;/code&gt; e &lt;code&gt;iss&lt;/code&gt;, aplica autorización de negocio en cada tool y escribe los tests hostiles antes de abrir el servidor a más clientes. Es menos vistoso que una demo de agente, pero es lo que evita convertir MCP en una llave maestra.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es OAuth 2.1 para MCP?
&lt;/h3&gt;

&lt;p&gt;Es el patrón de autorización que permite a un cliente MCP obtener un access token limitado y a un servidor MCP remoto validarlo antes de exponer tools o recursos protegidos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Necesita OAuth un servidor MCP local por STDIO?
&lt;/h3&gt;

&lt;p&gt;Normalmente no. Un servidor local puede usar credenciales del entorno o de una librería local; OAuth está pensado sobre todo para transportes HTTP remotos donde cliente y servidor no comparten una frontera de confianza.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué es Protected Resource Metadata en MCP?
&lt;/h3&gt;

&lt;p&gt;Es un documento JSON del servidor MCP que declara el resource identifier, los authorization servers y scopes. El cliente lo descubre desde &lt;code&gt;WWW-Authenticate&lt;/code&gt; o una ruta &lt;code&gt;/.well-known/&lt;/code&gt; para iniciar OAuth correctamente.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Basta con validar la firma del JWT?
&lt;/h3&gt;

&lt;p&gt;No. También debes validar issuer, audiencia, expiración y scopes, y luego aplicar autorización de negocio por usuario, tenant y recurso concreto en la tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo usar Dynamic Client Registration en MCP?
&lt;/h3&gt;

&lt;p&gt;Puede ser un fallback de compatibilidad. La especificación actual prefiere pre-registro cuando existe relación previa y Client ID Metadata Documents cuando cliente y servidor no se conocen de antemano.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿OAuth protege contra prompt injection?
&lt;/h3&gt;

&lt;p&gt;No directamente. OAuth limita quién puede invocar capacidades; sigue siendo necesario tratar contenido externo como no confiable, validar argumentos y exigir aprobación para efectos sensibles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo proteger un servidor MCP remoto con OAuth 2.1
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delimitar recurso.&lt;/strong&gt; Define una URL HTTPS estable para el endpoint MCP que será la audiencia esperada del token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diseñar scopes.&lt;/strong&gt; Separa lectura, escritura y acciones de alto impacto; evita permisos globales basados en nombres de tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publicar metadata.&lt;/strong&gt; Sirve Protected Resource Metadata con resource exacto, issuer permitido y scopes soportados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emitir challenge.&lt;/strong&gt; Devuelve 401 con &lt;code&gt;WWW-Authenticate&lt;/code&gt; y &lt;code&gt;resource_metadata&lt;/code&gt; cuando no haya token o falte scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configurar OAuth.&lt;/strong&gt; Usa Authorization Code con PKCE, discovery OAuth/OIDC y redirect URIs registrados con coincidencia exacta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validar access token.&lt;/strong&gt; Comprueba firma o introspección, issuer, audiencia, expiración, scopes y claim de tenant en cada llamada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autorizar tool.&lt;/strong&gt; Evalúa usuario, tenant, rol, ID de recurso y estado de negocio antes de llamar a sistemas aguas abajo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditar sin secretos.&lt;/strong&gt; Registra actor, client ID, tool, recurso y resultado; redacta token, código y cabeceras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar denegaciones.&lt;/strong&gt; Añade casos de token de otra audiencia, scope insuficiente, issuer falso, tenant cruzado y mutación sin aprobación.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization" rel="noopener noreferrer"&gt;MCP Authorization specification (2026-07-28)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/authorization" rel="noopener noreferrer"&gt;MCP: Understanding Authorization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices" rel="noopener noreferrer"&gt;MCP Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;MCP 2026-07-28 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc9728" rel="noopener noreferrer"&gt;RFC 9728: OAuth Protected Resource Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8414" rel="noopener noreferrer"&gt;RFC 8414: OAuth Authorization Server Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/" rel="noopener noreferrer"&gt;OAuth 2.1 draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/" rel="noopener noreferrer"&gt;OAuth Client ID Metadata Document draft&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad, permisos y supply chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-outputschema-structuredcontent-agentes/" rel="noopener noreferrer"&gt;MCP outputSchema y structuredContent para agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-apps-ui-interactiva-agentes/" rel="noopener noreferrer"&gt;MCP Apps: UI interactiva para tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para agentes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>Ollama en producción: Docker, privacidad, API compatible y límites que sí importan</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:18:43 +0000</pubDate>
      <link>https://dev.to/khavel/ollama-en-produccion-docker-privacidad-api-compatible-y-limites-que-si-importan-3737</link>
      <guid>https://dev.to/khavel/ollama-en-produccion-docker-privacidad-api-compatible-y-limites-que-si-importan-3737</guid>
      <description>&lt;p&gt;Ollama hace fácil ejecutar un modelo local; llevarlo a producción consiste en decidir qué puede alcanzar la red, quién llama a la API, cómo se versionan modelos y cuándo dejar de fingir que local equivale a seguro.&lt;/p&gt;

&lt;p&gt;Ollama en producción es un runtime para servir modelos abiertos localmente mediante una API HTTP. No es un producto de privacidad ni una plataforma de agentes completa: es la capa de inferencia que debes encerrar detrás de autenticación, límites, red y observabilidad propias.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;Ollama en producción&lt;/code&gt;. La intención es práctica: desplegar Ollama con Docker, usar su API (o la compatibilidad con OpenAI), decidir dónde guardar modelos y evitar exponer el puerto 11434 a una red que no controlas.&lt;/p&gt;

&lt;p&gt;Mi postura: úsalo primero para un caso interno, acotado y medible —clasificar tickets, resumir documentación permitida o un copiloto de bajo riesgo—. Si empiezas exponiendo un endpoint sin identidad, presupuesto ni trazas porque «el modelo está en tu máquina», has trasladado el riesgo; no lo has reducido.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qué es Ollama y qué no resuelve
&lt;/h3&gt;

&lt;p&gt;Ollama descarga y ejecuta modelos en una máquina y expone una API local. Su valor para un equipo es reducir fricción entre una aplicación y modelos abiertos: puedes hacer &lt;code&gt;pull&lt;/code&gt;, servir chat, embeddings o tools y conservar un contrato HTTP estable cerca de tus datos o de tu entorno de desarrollo.&lt;/p&gt;

&lt;p&gt;No sustituye un gateway de identidad, una política de datos, un sistema de secretos, un evaluador, un vector store ni un control de acceso por tenant. Tampoco hace seguro un prompt hostil: un modelo local puede seguir filtrar datos que le des, obedecer instrucciones inadecuadas o generar una acción errónea si tu aplicación se lo permite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Piensa en Ollama como piensas en Postgres: un componente importante, no la arquitectura completa. La pregunta sana no es «¿puedo correr un LLM en local?», sino «¿qué petición autenticada puede usar qué modelo, sobre qué datos, con qué límite y cómo demostraré qué pasó?».&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7uwnph7obpxdpc8kj9rw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7uwnph7obpxdpc8kj9rw.png" alt="Diagrama conceptual con una aplicación, un gateway de autenticación y límite de tasa, un contenedor de inferencia Ollama, un volumen persistente de modelos y un colector de observabilidad dentro de una frontera de red" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Una arquitectura mínima separa al consumidor del runtime: el gateway aplica identidad y cuotas; Ollama sirve inferencia; el volumen conserva modelos; y la telemetría permite operar sin registrar prompts completos por defecto.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Arquitectura mínima que sí desplegaría
&lt;/h3&gt;

&lt;p&gt;La versión pequeña tiene cuatro piezas. La aplicación llama a un backend o gateway propio; ese borde autentica al usuario o servicio, limita tasa y tamaño, decide el modelo permitido y crea una traza. Solo entonces llama a Ollama en una red privada. El volumen de modelos es persistente y el sistema de métricas recibe duración, tokens o campos de uso disponibles, modelo, estado y errores, no necesariamente el prompt literal.&lt;/p&gt;

&lt;p&gt;El puerto de Ollama debe quedar en loopback o en una red de contenedores no enrutable desde Internet. Si necesitas acceso remoto, publica el gateway con TLS y autenticación; no conviertas &lt;code&gt;11434&lt;/code&gt; en tu API pública. Esta separación también te deja cambiar de runtime, enrutar una parte del tráfico a un proveedor externo o apagar un modelo problemático sin editar cada cliente.&lt;/p&gt;

&lt;p&gt;Para varios tenants, el aislamiento no sale gratis por ejecutar local. Mantén la identidad y la autorización fuera del prompt: filtra documentos antes de construir contexto, utiliza credenciales de servicio de mínimo privilegio y no aceptes que el cliente elija libremente modelo, URL de herramientas o parámetros que multiplican coste.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Despliegue Docker seguro por defecto
&lt;/h3&gt;

&lt;p&gt;Este &lt;code&gt;compose.yaml&lt;/code&gt; no intenta resolver alta disponibilidad. Sí evita el error más común: publicar Ollama en todas las interfaces. El binding explícito a &lt;code&gt;127.0.0.1&lt;/code&gt; hace que el host local pueda inspeccionarlo, pero no lo anuncia a la LAN. Conserva los modelos en un volumen para que una recreación del contenedor no obligue a descargarlos de nuevo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;compose.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:11434:11434"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama-models:/root/.ollama&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;/dev/null&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arranca con &lt;code&gt;docker compose up -d&lt;/code&gt;, descarga un modelo desde una sesión administrativa con &lt;code&gt;docker exec -it &amp;lt;contenedor&amp;gt; ollama pull llama3.2&lt;/code&gt; y prueba &lt;code&gt;curl http://127.0.0.1:11434/api/tags&lt;/code&gt;. En hardware acelerado, sigue la sección de GPU de la documentación oficial y verifica en logs qué backend se cargó: asumir que hay GPU es una forma cara de descubrir que todo está sirviendo por CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  API: úsala directa o mantén compatibilidad OpenAI
&lt;/h2&gt;

&lt;p&gt;La API nativa de Ollama es la mejor elección cuando controlas el cliente y quieres sus conceptos tal cual. La compatibilidad parcial con OpenAI es útil para migrar un SDK existente o para que tu gateway tenga una interfaz uniforme, pero no debe llevarte a asumir paridad total de endpoints, estado o campos. Lee la tabla de compatibilidad de la versión que despliegues y prueba los casos que consumes.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Un cliente Python mínimo puede hablar por la ruta compatible sin poner una clave secreta local. La cadena &lt;code&gt;api_key&lt;/code&gt; existe porque el SDK la exige; no equivale a autenticar tu servidor. La autenticación real debe estar en el gateway delante de ese endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;client.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:11434/v1/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# requerido por el SDK; no autentica Ollama
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Responde solo con JSON valido.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;ticket&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;T-42&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La validación continúa después del modelo. Parsea el JSON con un schema real, rechaza campos inesperados y no conviertas una respuesta del LLM directamente en SQL, shell, HTTP o una llamada mutante. Ese límite importa igual con modelo local, cloud o híbrido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modelos y Modelfile: versiona el contrato, no solo el nombre
&lt;/h2&gt;

&lt;p&gt;Un nombre de modelo flotante no es una garantía de comportamiento. Para un workflow serio, fija versión de imagen, modelo y configuración que realmente evaluaste. Guarda también el hash de tu prompt, la plantilla, el tamaño de contexto, parámetros relevantes y el dataset de evaluación. Si cualquiera cambia, tienes una nueva variante de producción.&lt;/p&gt;

&lt;p&gt;Un &lt;code&gt;Modelfile&lt;/code&gt; permite partir de un modelo y declarar parámetros o instrucciones. Es útil para una política de salida estable o un contexto concreto, pero no es una frontera de seguridad: un usuario todavía puede intentar desviar la tarea y tu aplicación sigue teniendo que validar resultados y permisos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modelfile&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; llama3.2&lt;/span&gt;
PARAMETER temperature 0.1
PARAMETER num_ctx 8192
SYSTEM """
Eres un clasificador de tickets internos.
Devuelve JSON con categoria, prioridad y evidencia breve.
No inventes datos que no aparezcan en la entrada.
"""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crea una etiqueta evaluable con &lt;code&gt;ollama create soporte-v1 -f Modelfile&lt;/code&gt;, ejecuta tu conjunto de casos y promociona esa etiqueta solo si pasa los gates de precisión, rechazo, latencia y coste de infraestructura. Una modificación de &lt;code&gt;num_ctx&lt;/code&gt; puede cambiar memoria y latencia de forma material; no es un ajuste cosmético.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Privacidad: qué mejora y qué sigue siendo tu problema
&lt;/h3&gt;

&lt;p&gt;Ejecutar inferencia dentro de tu red puede reducir la exposición a un proveedor externo, pero solo si el input, los logs, el almacenamiento de modelos, los backups, los proxies y la observabilidad respetan el mismo límite. Una traza con prompt completo en un SaaS externo invalida una parte importante de la decisión, aunque el token se haya calculado en local.&lt;/p&gt;

&lt;p&gt;Define una clasificación de datos antes de permitirlos: público interno, confidencial, datos personales, secretos y datos prohibidos. Para cada clase, decide si puede entrar al prompt, cuánto tiempo se conserva, quién puede ver logs y qué hacer ante borrado o incidente. Si no puedes contestar esas preguntas, usa datos sintéticos hasta poder hacerlo.&lt;/p&gt;

&lt;p&gt;Y no confundas ausencia de tráfico externo con seguridad. Prompt injection indirecta, documentos maliciosos, exfiltración mediante herramientas y una respuesta alucinada siguen siendo riesgos de la aplicación. Mantén tools con allowlist, separa lectura de escritura y exige aprobación humana para efectos externos.&lt;/p&gt;
&lt;h3&gt;
  
  
  Coste y capacidad: local no significa gratis
&lt;/h3&gt;

&lt;p&gt;El coste se mueve de una factura por token a hardware, electricidad, VRAM o RAM, tiempo de operación, disco y capacidad ociosa. Por eso conviene medir por workload: tokens por segundo, latencia p50/p95, cola, memoria, modelo cargado, errores de carga y porcentaje de requests abandonadas. El número que importa no es solo «cuánto tarda una respuesta», sino cuánto tarda una petición útil bajo concurrencia real.&lt;/p&gt;

&lt;p&gt;Empieza con una concurrencia pequeña y un modelo que quepa con margen en tu máquina. Aumentar contexto, paralelismo o modelos residentes puede deteriorar latencia o provocar presión de memoria. Pon timeout en el gateway, backpressure para la cola y un error claro cuando no hay capacidad, en lugar de dejar peticiones colgadas hasta que el caller reintente en cascada.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Si un caso requiere gran contexto, razonamiento fuerte o SLA estricto, un runtime local puede no ser la mejor capa principal. Diseña una ruta explícita: modelo local para clasificación y extracción barata; proveedor remoto aprobado para los casos que justifiquen coste y datos permitidos; y un fallback humano cuando el resultado no es seguro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observabilidad y evaluación antes de ampliar tráfico
&lt;/h2&gt;

&lt;p&gt;Registra un ID de petición, usuario o servicio pseudonimizado, modelo, versión de prompt, latencia, cola, resultado de schema, tool calls y motivo de denegación. Evita convertir el tracing en un segundo repositorio de secretos: usa redacción, hashes o muestras aprobadas para el contenido sensible.&lt;/p&gt;

&lt;p&gt;Antes de cambiar modelo, cuantización, contexto o Modelfile, ejecuta un dataset fijo con casos normales, ambiguos y hostiles. Mide tarea correcta, formato válido, evidencia, rechazo apropiado, coste de infraestructura y latencia. Haz un canary pequeño y compara contra la versión anterior; una demo manual no detecta regresiones silenciosas.&lt;/p&gt;

&lt;p&gt;El enlace con la guía de evaluación RAG es deliberado: aunque no haya retrieval, necesitas una disciplina de dataset, baseline y gates. Sin ese contrato, cada actualización de modelo se convierte en una apuesta sobre usuarios reales.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Errores que veo al desplegar Ollama
&lt;/h3&gt;

&lt;p&gt;Publicar &lt;code&gt;0.0.0.0:11434&lt;/code&gt; y confiar en que la red corporativa ya es un control de acceso.&lt;/p&gt;

&lt;p&gt;Permitir que el frontend elija cualquier modelo, tamaño de contexto o tool sin pasar por backend.&lt;/p&gt;

&lt;p&gt;Descargar modelos sin inventario, licencia revisada, versión fijada ni prueba de comportamiento.&lt;/p&gt;

&lt;p&gt;Guardar prompts completos, credenciales y documentos sensibles en logs por defecto.&lt;/p&gt;

&lt;p&gt;Tratar el system prompt o el Modelfile como si fueran autorización de seguridad.&lt;/p&gt;

&lt;p&gt;Medir una respuesta aislada en un portátil y declarar que hay capacidad de producción.&lt;/p&gt;

&lt;p&gt;Cambiar modelo y prompt a la vez; cuando baja la calidad, nadie sabe qué regresó.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Checklist de salida a producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ollama queda en loopback o red privada; no hay puerto de inferencia abierto a Internet.&lt;/li&gt;
&lt;li&gt;Un gateway autentica clientes, aplica cuotas, timeouts y un allowlist de modelos.&lt;/li&gt;
&lt;li&gt;Los modelos, Modelfiles y parámetros usados están versionados y evaluados.&lt;/li&gt;
&lt;li&gt;Los datos que entran al prompt tienen clasificación, retención y redacción definida.&lt;/li&gt;
&lt;li&gt;Las respuestas pasan schema validation antes de llegar a sistemas internos.&lt;/li&gt;
&lt;li&gt;Las tools tienen permisos mínimos; las acciones mutantes requieren confirmación y auditoría.&lt;/li&gt;
&lt;li&gt;Hay métricas de latencia, capacidad, errores y resultado de evaluación sin registrar secretos por defecto.&lt;/li&gt;
&lt;li&gt;Un canary y un rollback permiten volver a la variante anterior sin editar todos los clientes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Ollama es muy útil cuando necesitas iterar con modelos abiertos cerca de tus sistemas y no quieres que cada equipo invente su propio launcher. Pero su ventaja se diluye si lo expones como una API anónima, no sabes qué modelo responde o registras indiscriminadamente todo el contexto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Mi recomendación es aburrida y eficaz: un modelo, una tarea interna de lectura, un gateway, un dataset de evaluación, una red privada y telemetría mínima. Cuando puedas explicar latencia, datos, permisos y rollback con la misma claridad que explicas &lt;code&gt;ollama run&lt;/code&gt;, entonces ya tienes una base para ampliar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es Ollama en producción?
&lt;/h3&gt;

&lt;p&gt;Es usar Ollama como runtime de inferencia para una aplicación real, con despliegue, red, identidad, límites, observabilidad, evaluación y rollback; no solo ejecutar un chat en local.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Ollama es privado por defecto?
&lt;/h3&gt;

&lt;p&gt;Ejecutar un modelo en tu infraestructura puede reducir exposición externa, pero no garantiza privacidad. Siguen importando los prompts, logs, backups, proxies, permisos, modelos descargados y herramientas conectadas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo exponer Ollama directamente a Internet?
&lt;/h3&gt;

&lt;p&gt;No es una buena arquitectura. Mantén el runtime en red privada y expón un gateway con TLS, autenticación, cuotas y validación de requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿La compatibilidad OpenAI de Ollama es completa?
&lt;/h3&gt;

&lt;p&gt;No. Facilita reutilizar parte de clientes y endpoints, pero debes comprobar las funciones y límites que necesita tu aplicación en la documentación y en tests de integración.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo reduzco el coste de Ollama?
&lt;/h3&gt;

&lt;p&gt;Mide tokens por segundo, latencia, cola, memoria y utilización; elige modelos que encajen en tu hardware, limita contexto y concurrencia, y enruta solo tareas justificadas al modelo más caro.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Un Modelfile protege contra prompt injection?
&lt;/h3&gt;

&lt;p&gt;No. Sirve para configurar un modelo, pero la defensa requiere tratar contenido externo como datos, validar salidas, limitar tools y aplicar permisos en el servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo desplegar Ollama para un primer workflow interno
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Acotar la tarea.&lt;/strong&gt; Empieza con una operación de lectura y bajo riesgo, como clasificación o extracción de documentos permitidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preparar red privada.&lt;/strong&gt; Ejecuta el contenedor con puerto en loopback o una red interna; no publiques el puerto de inferencia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistir y descargar.&lt;/strong&gt; Monta volumen de modelos, descarga una versión elegida y anota imagen, modelo y parámetros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interponer gateway.&lt;/strong&gt; Autentica al consumidor, limita tasa, fija modelos admitidos, define timeout y crea una traza por request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versionar contrato.&lt;/strong&gt; Crea Modelfile si hace falta, guarda prompt y schema de salida, y etiqueta la variante evaluada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validar resultados.&lt;/strong&gt; Parsea la salida con schema y separa cualquier tool o efecto externo de la respuesta del modelo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir baseline.&lt;/strong&gt; Ejecuta dataset de casos normales, ambiguos y hostiles; guarda calidad, latencia, capacidad y fallos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desplegar canary.&lt;/strong&gt; Envía una fracción pequeña de tráfico, compara con baseline y conserva rollback a la variante previa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/quickstart" rel="noopener noreferrer"&gt;Ollama Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/api/introduction" rel="noopener noreferrer"&gt;Ollama API introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/api/openai-compatibility" rel="noopener noreferrer"&gt;Ollama OpenAI compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/modelfile" rel="noopener noreferrer"&gt;Ollama Modelfile reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/faq" rel="noopener noreferrer"&gt;Ollama FAQ: server, proxy and Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ollama/ollama" rel="noopener noreferrer"&gt;Ollama official repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/containers/run/" rel="noopener noreferrer"&gt;Docker: run containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/litellm-proxy-gateway-llm-costes/" rel="noopener noreferrer"&gt;LiteLLM Proxy: gateway IA, costes y modelos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/evaluacion-rag-produccion-metricas-datasets/" rel="noopener noreferrer"&gt;Evaluación RAG en producción&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad y permisos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>Make your CI fail before your model does</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Wed, 05 Aug 2026 11:45:11 +0000</pubDate>
      <link>https://dev.to/khavel/make-your-ci-fail-before-your-model-does-ecm</link>
      <guid>https://dev.to/khavel/make-your-ci-fail-before-your-model-does-ecm</guid>
      <description>&lt;p&gt;Today, August 5 2026, &lt;code&gt;claude-opus-4-1&lt;/code&gt; retires. Anthropic said so on June 5, on their model-deprecations page, with the date in plain text. Two months of notice.&lt;/p&gt;

&lt;p&gt;If that id is hard-coded in a repo somewhere, nothing in the build system found out. Tests passed all summer. The linter had no opinion. The dependency bot, which will open a PR because a transitive dev dependency moved from 4.2.1 to 4.2.2, has no idea the model your product is built on has an expiry date on someone else's calendar.&lt;/p&gt;

&lt;p&gt;That is the actual shape of the problem: &lt;strong&gt;a model dying is a calendar event, not a code change.&lt;/strong&gt; Nothing in CI watches a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your provider's email doesn't cover you
&lt;/h2&gt;

&lt;p&gt;It does exist. Anthropic, OpenAI and Google all email deprecation notices — they are contractually motivated to. But they email &lt;strong&gt;the account owner&lt;/strong&gt;: whoever's card is on the org. Not the repo. Not the pull request. Not the person who pinned the id eight months ago and left.&lt;/p&gt;

&lt;p&gt;So the notice lands in a billing inbox, and the id sits in &lt;code&gt;src/llm.ts&lt;/code&gt; until the call starts 404ing. Between those two events there are usually months where a build could have told you, and didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, from today's catalog
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://aimodelwatch.dev" rel="noopener noreferrer"&gt;AI Model Watch&lt;/a&gt;, a daily-verified catalog of model prices, context limits and lifecycle dates, every row carrying the provider URL it was read from. Pulled from the live feed while writing this (&lt;code&gt;updated: 2026-08-05&lt;/code&gt;, 204 models):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;74 of the 204&lt;/strong&gt; are already &lt;code&gt;deprecated&lt;/code&gt; or &lt;code&gt;retired&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12&lt;/strong&gt; carry a retirement date inside the next 90 days (13 if you count the one retiring today).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not add those together. &lt;strong&gt;9 of the 12 are already inside the 74&lt;/strong&gt; — being deprecated and being near your retirement date is the normal case, not two separate populations. The distinct total is &lt;strong&gt;77&lt;/strong&gt;, not 86. Two counts in one paragraph is a set problem, and the word joining them is a claim like any other.&lt;/p&gt;

&lt;p&gt;The interesting three are the ones that &lt;em&gt;aren't&lt;/em&gt; in the 74. &lt;code&gt;qwen3-max&lt;/code&gt; is &lt;code&gt;ga&lt;/code&gt; today, priced today, sitting in the current docs looking entirely healthy — and Alibaba's own deprecation table retires it on &lt;strong&gt;September 8&lt;/strong&gt;, pointing at &lt;code&gt;qwen3.7-max&lt;/code&gt;. A status field would never have warned you. Only the date does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirty seconds to a check
&lt;/h2&gt;

&lt;p&gt;One file, no dependencies, no key, no signup, Node 18+:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sLO&lt;/span&gt; https://aimodelwatch.dev/ci/check-models.mjs
node check-models.mjs &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--scan&lt;/code&gt; walks your source and picks out anything that looks like a model id it knows, so there is nothing to configure on the first run. Here is a real run against a three-line fixture — output as it came out, only the Windows path separators normalized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node check-models.mjs &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;check-models 1.1.0 — feed updated 2026-08-05, 204 models
scanned . — found 3 model ids in your source
  claude-opus-4-1  src/llm.ts:2
  gemini-2.5-flash  src/llm.ts:4
  qwen3-max  src/llm.ts:3
checked 3 models · retirement threshold 90 days

✖ claude-opus-4-1: Claude Opus 4.1 is DEPRECATED. Retires 2026-08-05 (in 0 days). Provider's stated replacement: claude-opus-4-8.
✖ qwen3-max: Qwen3-Max retires 2026-09-08 — 34 days away (threshold 90). Provider's stated replacement: qwen3.7-max.
✖ gemini-2-5-flash: Gemini 2.5 Flash is DEPRECATED. Retires 2026-10-16 (in 72 days). Provider's stated replacement: gemini-3.6-flash.

3 problems, 0 warnings. Details: https://aimodelwatch.dev/deprecations

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ids, three providers, three different failure modes: one that expires today, one still marked GA with a date five weeks out, one deprecated with a quarter left. Exit 1 on all of them.&lt;/p&gt;

&lt;p&gt;(The id in my source is &lt;code&gt;gemini-2.5-flash&lt;/code&gt; and the finding comes back as &lt;code&gt;gemini-2-5-flash&lt;/code&gt; — providers write the same model both ways depending on the page, so ids are matched in a normalized form. Worth knowing if you grep the JSON report.)&lt;/p&gt;

&lt;h2&gt;
  
  
  In GitHub Actions
&lt;/h2&gt;

&lt;p&gt;There is an action, so this is the entire setup — no install step, no &lt;code&gt;setup-node&lt;/code&gt;, no inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/model-lifecycle.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;model lifecycle&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Khavel/check-models-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no inputs it walks the repo, finds every model id in the source, and checks all of them. Findings come back as inline &lt;code&gt;::error&lt;/code&gt; annotations on the offending line plus a job-summary table. The action vendors the script, so &lt;code&gt;@v1&lt;/code&gt; pins the code while the catalog stays live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cron line is the half that matters.&lt;/strong&gt; A &lt;code&gt;pull_request&lt;/code&gt; trigger only helps on days somebody opens a PR. Deprecations are announced on days nobody opens a PR — that is the entire nature of a calendar event. A weekly run is what turns "we found out in August" into "we found out in June".&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: prices move too
&lt;/h2&gt;

&lt;p&gt;Nobody emails you when a model gets cheaper or dearer. There is no deprecation notice for a 40% output-price hike; the invoice just changes shape at the end of the month.&lt;/p&gt;

&lt;p&gt;So pin the numbers you costed the product on, and commit the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node check-models.mjs &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--pin&lt;/span&gt; .amw-pin.json &lt;span class="nt"&gt;--update-pin&lt;/span&gt;   &lt;span class="c"&gt;# once, then commit&lt;/span&gt;
node check-models.mjs &lt;span class="nt"&gt;--scan&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--pin&lt;/span&gt; .amw-pin.json                &lt;span class="c"&gt;# every build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pin file is small and diffable — status, the three prices, &lt;code&gt;retires_on&lt;/code&gt;, the stated replacement, per model. When a value moves away from what you pinned, it shows up as a build failure with both numbers in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✖ qwen3-max: Qwen3-Max input price ($/1M) changed since your pin: 0.8 → 1.2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That one I produced by editing the pin to an old value — I am not going to fake a price change to make a point. The mechanism is what's real: it compares your committed snapshot against today's catalog.)&lt;/p&gt;

&lt;h2&gt;
  
  
  One sharp edge, stated up front
&lt;/h2&gt;

&lt;p&gt;GitHub does not evaluate a composite action's outputs when the action fails the job. So &lt;code&gt;steps.*.outputs.*&lt;/code&gt; come back empty on exactly the path you care about — the failing one.&lt;/p&gt;

&lt;p&gt;I could not fix that from inside a composite action, so instead the JSON report is written unconditionally: point &lt;code&gt;report-path&lt;/code&gt; somewhere and read it with &lt;code&gt;if: always()&lt;/code&gt;, or set &lt;code&gt;warn-only: true&lt;/code&gt; and branch on the outputs yourself. Worth knowing before you build a Slack notification on top of the outputs and wonder why it posts empty messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's underneath
&lt;/h2&gt;

&lt;p&gt;The check reads a free JSON feed — no key, no signup, CORS open, and every row carries the provider page the values were read from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://aimodelwatch.dev/api/models.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://aimodelwatch.dev/api/deprecations.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in it is what the provider &lt;strong&gt;states&lt;/strong&gt;, taken from its own documentation. Where a provider publishes nothing, the field is &lt;code&gt;null&lt;/code&gt; and the check stays quiet rather than guessing — an invented retirement date that fails someone's build would be much worse than a missing one.&lt;/p&gt;

&lt;p&gt;If you would rather not put a third-party action in your pipeline, the script is one file with no dependencies and works on any CI. &lt;code&gt;curl&lt;/code&gt; it and run it.&lt;/p&gt;

&lt;p&gt;Either way: the check costs about two lines, and the thing it catches is a model that stops existing while your tests are green.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs and options: &lt;a href="https://aimodelwatch.dev/ci" rel="noopener noreferrer"&gt;https://aimodelwatch.dev/ci&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Action source: &lt;a href="https://github.com/Khavel/check-models-action" rel="noopener noreferrer"&gt;https://github.com/Khavel/check-models-action&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The feed: &lt;a href="https://aimodelwatch.dev/api/models.json" rel="noopener noreferrer"&gt;https://aimodelwatch.dev/api/models.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>MCP Apps: cómo añadir interfaces interactivas a tools MCP sin abrir un agujero de seguridad</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Thu, 30 Jul 2026 10:17:31 +0000</pubDate>
      <link>https://dev.to/khavel/mcp-apps-como-anadir-interfaces-interactivas-a-tools-mcp-sin-abrir-un-agujero-de-seguridad-3110</link>
      <guid>https://dev.to/khavel/mcp-apps-como-anadir-interfaces-interactivas-a-tools-mcp-sin-abrir-un-agujero-de-seguridad-3110</guid>
      <description>&lt;p&gt;MCP Apps permite que una tool devuelva una interfaz interactiva dentro del chat. Es útil para aprobar, explorar y decidir; no para saltarse permisos ni convertir el agente en una web embebida sin controles.&lt;/p&gt;

&lt;p&gt;MCP Apps es una extensión de Model Context Protocol para que un servidor devuelva una interfaz interactiva —un dashboard, formulario, tabla o flujo de aprobación— dentro de un host de chat compatible. La UI vive en un iframe sandboxed y habla con el host mediante mensajes controlados; no obtiene acceso directo al DOM, cookies o almacenamiento del host.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;MCP Apps&lt;/code&gt;. La intención es práctica: entender cuándo una tool necesita UI, cómo conectar la vista con un servidor MCP y qué límites de seguridad son imprescindibles antes de ponerla delante de usuarios o datos reales.&lt;/p&gt;

&lt;p&gt;Mi postura: una UI MCP tiene sentido cuando reduce ambigüedad humana, no cuando maquilla una tool demasiado poderosa. Un formulario de aprobación, una tabla filtrable o un explorador de resultados puede evitar decenas de turnos. Una mini-aplicación con acceso libre a red y tools de escritura solo multiplica superficie de ataque.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qué es una MCP App y qué no es
&lt;/h3&gt;

&lt;p&gt;Un servidor MCP normal expone tools, resources y prompts. La respuesta de una tool suele ser texto y, opcionalmente, &lt;code&gt;structuredContent&lt;/code&gt;. Una MCP App añade una resource de UI, normalmente identificada con &lt;code&gt;ui://&lt;/code&gt;, que el host puede renderizar junto al resultado. La vista recibe datos del resultado y puede pedir acciones al host por un puente de mensajes.&lt;/p&gt;

&lt;p&gt;No es una nueva forma de hacer una SPA pública. La conversación sigue siendo el contexto principal; la interfaz es una mejora progresiva para la parte que una lista de texto resuelve mal. Si el host no soporta MCP Apps, la tool debe seguir devolviendo una respuesta textual útil. Ese fallback no es un detalle: es el contrato de portabilidad.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tampoco es una autorización implícita. Que la vista muestre un botón no significa que pueda ejecutar una operación. El servidor debe validar usuario, tenant, argumentos y política igual que lo haría si la llamada viniera de un cliente HTTP ordinario.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqmtxa2hzi6r184to6qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqmtxa2hzi6r184to6qq.png" alt="Arquitectura de una MCP App: host de chat, interfaz en iframe sandboxed, puente de mensajes y servidor MCP con tools y datos" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La UI no se conecta libremente al host: recibe contexto y solicita acciones a través de un bridge; el host sigue decidiendo qué capacidades permite.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  La arquitectura: servidor, host y vista
&lt;/h3&gt;

&lt;p&gt;Hay tres piezas. El servidor MCP registra una tool y una resource HTML. El host descubre ambas, ejecuta la tool y, si soporta la extensión, monta la resource en un iframe aislado. La vista es un cliente pequeño: se inicializa, recibe input y resultado de la tool, y puede solicitar &lt;code&gt;tools/call&lt;/code&gt;, recursos o acciones del host según sus capacidades.&lt;/p&gt;

&lt;p&gt;La separación entre &lt;code&gt;content&lt;/code&gt; y &lt;code&gt;structuredContent&lt;/code&gt; importa. &lt;code&gt;content&lt;/code&gt; es la explicación que puede necesitar el modelo y sirve como fallback textual. &lt;code&gt;structuredContent&lt;/code&gt; es un objeto pensado para renderizar: IDs, series de datos, estados y filas. No metas en el contexto del modelo 3.000 filas que solo necesita pintar una tabla; entrega una síntesis textual y datos estructurados a la UI.&lt;/p&gt;

&lt;p&gt;El host es la frontera de confianza. Puede restringir llamadas, enlaces externos, modo de visualización y capacidades de la app. Diseña la vista asumiendo que no tiene permiso para todo y que una petición puede ser rechazada; es una propiedad sana, no una limitación incómoda.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuándo una tool necesita UI
&lt;/h3&gt;

&lt;p&gt;Usaría MCP Apps para explorar datos con filtros, comparar opciones, revisar un diff, rellenar un formulario de aprobación, visualizar un pipeline o confirmar una acción con consecuencias. En todos esos casos hay estado visual, selección humana o demasiada información para que el modelo la resuma sin perder control.&lt;/p&gt;

&lt;p&gt;No la usaría para una búsqueda de documentación, una consulta determinista, una acción de una línea o un workflow que nadie necesita inspeccionar. Una respuesta textual o &lt;code&gt;structuredContent&lt;/code&gt; basta y es más simple de probar. La UI también introduce lifecycle, accesibilidad, CSP, degradación y una matriz de hosts; no la añadas solo porque es nueva.&lt;/p&gt;

&lt;p&gt;La pregunta de producto es concreta: ¿qué decisión humana mejora al ver y manipular este resultado? Si no puedes responderla, conserva la tool como texto. Si la respuesta es revisar, seleccionar o aprobar, una UI embebida puede reducir errores y turnos innecesarios.&lt;/p&gt;

&lt;p&gt;server.ts — tool con fallback textual y datos para la vista&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "release-dashboard", version: "1.0.0" });

server.registerResource(
  "release-view",
  "ui://release-dashboard/view.html",
  {},
  async () =&amp;gt; ({
    contents: [{
      uri: "ui://release-dashboard/view.html",
      mimeType: "text/html;profile=mcp-app",
      text: await loadBundledHtml()
    }]
  })
);

server.registerTool(
  "list_release_risks",
  {
    title: "Riesgos de despliegue",
    inputSchema: { service: "string" },
    _meta: { ui: { resourceUri: "ui://release-dashboard/view.html" } }
  },
  async ({ service }, extra) =&amp;gt; {
    const user = await requireAuthorizedUser(extra);
    const risks = await readRisksForTenant(user.tenantId, service);
    return {
      content: [{ type: "text", text: `Hay ${risks.length} riesgos abiertos para ${service}.` }],
      structuredContent: { service, risks: risks.map(toSafeViewModel) }
    };
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El ejemplo deja dos decisiones visibles. La resource no se inventa una URL web: usa un URI &lt;code&gt;ui://&lt;/code&gt; registrado. Y la tool comprueba identidad y tenant antes de leer datos; &lt;code&gt;structuredContent&lt;/code&gt; solo contiene el modelo de vista seguro. El paquete &lt;code&gt;@modelcontextprotocol/ext-apps&lt;/code&gt; ofrece helpers para registrar tools/resources y construir la vista, pero no sustituye esas validaciones.&lt;/p&gt;

&lt;h2&gt;
  
  
  La vista: trata el iframe como un cliente no confiable
&lt;/h2&gt;

&lt;p&gt;La vista debe inicializarse con el bridge, esperar los eventos del host y renderizar solo datos validados. Su trabajo es presentar y recoger intención del usuario, no decidir permisos. Cuando el usuario pulsa aprobar, la vista llama a una tool estrecha con un ID; el servidor vuelve a comprobar que la persona puede aprobar ese recurso y que el estado sigue siendo válido.&lt;/p&gt;

&lt;p&gt;Evita pasar secretos, tokens de larga vida o documentos completos en el HTML de la resource. El iframe aislado reduce privilegios, pero no convierte datos sensibles en inocuos. Envía el mínimo necesario, aplica redacción por tenant y considera que cualquier dato mostrado puede ser copiado por el usuario autorizado.&lt;/p&gt;

&lt;p&gt;Para acciones de escritura, modela una transición explícita: &lt;code&gt;preview&lt;/code&gt; → &lt;code&gt;confirm&lt;/code&gt; → &lt;code&gt;execute&lt;/code&gt;. La UI puede enseñar el impacto y pedir confirmación; el servidor debe usar un idempotency key y rechazar operaciones repetidas o estados caducados. Es el mismo patrón que usarías en una API de pagos, solo que aquí el disparador nació dentro de un chat.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  CSP, red y enlaces: el límite que suele olvidarse
&lt;/h3&gt;

&lt;p&gt;Una MCP App declara sus necesidades de red y el host puede aplicar esa política. Empieza con una CSP restrictiva: sin conexiones externas si no son necesarias; dominios concretos para API o assets; nada de comodines por comodidad. Si la app necesita datos, es preferible que los pida mediante una tool auditada antes que abrir &lt;code&gt;connect-src \*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No dejes que HTML o markdown procedente de tickets, documentos o usuarios llegue a la vista como markup confiable. Sanitiza, usa &lt;code&gt;textContent&lt;/code&gt; para texto, limita URLs y evita inyectar plantillas dinámicas. Prompt injection no desaparece por mover el resultado a una UI: el contenido externo puede seguir intentando influir en el humano o en llamadas posteriores.&lt;/p&gt;

&lt;p&gt;Abrir un enlace externo debe ser una capacidad explícita del host, no un efecto lateral de renderizar una celda de tabla. Enseña dominio y destino cuando una acción saque al usuario de la conversación. La fricción pequeña es preferible a una redirección silenciosa desde un panel que parece interno.&lt;/p&gt;
&lt;h3&gt;
  
  
  Compatibilidad progresiva y testing
&lt;/h3&gt;

&lt;p&gt;El soporte de MCP Apps varía entre hosts y puede cambiar. Por eso prueba dos salidas: una sesión con UI y otra con solo texto. El contenido textual debe explicar resultado, límites y siguiente acción sin depender de la interfaz. Si el host no renderiza la vista, la tool no puede convertirse en un callejón sin salida.&lt;/p&gt;

&lt;p&gt;Automatiza tests de contrato en el servidor: schema de entrada, autorización, filtrado por tenant, modelo de &lt;code&gt;structuredContent&lt;/code&gt;, errores y doble ejecución. En la vista, prueba que una respuesta parcial, vacía o denegada no bloquee el chat. Y ensaya manualmente la interacción con los hosts que de verdad vas a soportar; no declares compatibilidad por haber visto un ejemplo funcionar en local.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mide utilidad, no solo clicks: cuántos turnos evita la UI, cuántas aprobaciones se revierten, qué operaciones se cancelan, cuánto tarda en aparecer el resultado y cuántas veces se usa el fallback textual. Si no reduce error o tiempo de decisión, una respuesta bien diseñada probablemente era mejor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;La tool devuelve una respuesta textual completa aunque el host no soporte UI.&lt;/li&gt;
&lt;li&gt;La resource usa un URI &lt;code&gt;ui://&lt;/code&gt; registrado y MIME type específico para MCP Apps.&lt;/li&gt;
&lt;li&gt;La vista recibe &lt;code&gt;structuredContent&lt;/code&gt; mínimo y no secretos ni datos de otros tenants.&lt;/li&gt;
&lt;li&gt;Cada tool de lectura o escritura revalida usuario, tenant, scopes y estado en servidor.&lt;/li&gt;
&lt;li&gt;Las acciones mutantes tienen preview, confirmación, idempotencia y auditoría.&lt;/li&gt;
&lt;li&gt;La CSP declara solo dominios imprescindibles; sin comodines ni scripts remotos no revisados.&lt;/li&gt;
&lt;li&gt;La UI trata todo contenido externo como datos y lo sanitiza antes de mostrarlo.&lt;/li&gt;
&lt;li&gt;Se prueba el fallback textual y la degradación en cada host objetivo.&lt;/li&gt;
&lt;li&gt;Logs guardan IDs, acción, resultado y denegaciones; no el contenido sensible por defecto.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;MCP Apps resuelve una carencia real: hay decisiones que una conversación textual explica mal. El valor no es poner un dashboard bonito dentro de un chat; es dar una superficie de revisión pequeña, contextual y reversible a una tool que ya tiene un contrato claro.&lt;/p&gt;

&lt;p&gt;Empezaría con una sola tool de lectura y una vista que haga una cosa excelente: filtrar incidencias, revisar resultados o comparar un plan. Mantén fallback textual, CSP corta, datos mínimos y calls de escritura separadas. Cuando eso sea operable, amplía. En agentes, cada pixel interactivo también es una superficie de permiso.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es MCP Apps?
&lt;/h3&gt;

&lt;p&gt;Es una extensión de Model Context Protocol que permite a un servidor MCP entregar una interfaz interactiva dentro de un host compatible, además del contenido textual y estructurado normal de una tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Una MCP App funciona en todos los clientes?
&lt;/h3&gt;

&lt;p&gt;No. El soporte depende del host. Por eso una tool debe seguir ofreciendo un fallback textual útil cuando la interfaz no se pueda renderizar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿La UI de MCP Apps puede acceder al DOM o las cookies del host?
&lt;/h3&gt;

&lt;p&gt;No debería. La arquitectura usa un iframe sandboxed y comunicación mediante un bridge de mensajes; el host conserva el control de capacidades.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo usar MCP Apps en lugar de una respuesta de texto?
&lt;/h3&gt;

&lt;p&gt;Cuando el usuario necesita explorar datos, seleccionar opciones, revisar un artefacto o aprobar una acción. Para consultas simples, texto o structuredContent suele ser más robusto.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo protejo una MCP App?
&lt;/h3&gt;

&lt;p&gt;Valida autorización en el servidor para cada tool, limita structuredContent, aplica CSP restrictiva, sanitiza datos externos, exige confirmación para escrituras y registra acciones sin guardar secretos por defecto.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo reutilizar una web existente como MCP App?
&lt;/h3&gt;

&lt;p&gt;Sí, si adaptas la vista al lifecycle y al bridge del host, declaras recursos y CSP, y conservas una salida textual. No presupongas que una SPA existente funciona segura dentro de un iframe MCP sin cambios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo crear una primera MCP App segura para una tool existente
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Elegir una decisión visual.&lt;/strong&gt; Selecciona una tool de lectura donde filtrar, comparar o aprobar aporte más que texto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definir fallback.&lt;/strong&gt; Escribe primero el content textual completo que recibirá un host sin soporte de UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registrar resource.&lt;/strong&gt; Publica una resource ui:// con HTML empaquetado y MIME type de MCP App.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devolver datos mínimos.&lt;/strong&gt; Añade structuredContent con un view model seguro, sin secretos ni campos de otros tenants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construir la vista.&lt;/strong&gt; Inicializa el bridge, renderiza estados de carga y trata respuestas denegadas o parciales como normales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Añadir llamada estrecha.&lt;/strong&gt; Si hay interacción, llama a una tool con IDs y valida usuario, tenant, scopes y estado en servidor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cerrar CSP.&lt;/strong&gt; Declara solo redes y capacidades imprescindibles; usa herramientas MCP antes que conexiones libres desde el iframe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar degradación.&lt;/strong&gt; Ejecuta los tests de contrato y comprueba la salida textual en hosts sin UI antes de anunciar soporte.
&amp;gt; ### Criterio técnico
&amp;gt;
&amp;gt; Un buen chunk en tiempo real no es el más corto ni el más semántico: es el que conserva evidencia, tiempo y estado suficiente para responder sin inventar continuidad.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/extensions/apps/overview" rel="noopener noreferrer"&gt;MCP Apps: overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/extensions/apps/build" rel="noopener noreferrer"&gt;MCP Apps: build guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html" rel="noopener noreferrer"&gt;MCP Apps: quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.extensions.modelcontextprotocol.io/api/documents/overview.html#security" rel="noopener noreferrer"&gt;MCP Apps: security model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apps.extensions.modelcontextprotocol.io/api/documents/authorization.html" rel="noopener noreferrer"&gt;MCP Apps: authorization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;MCP Apps SDK and examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices" rel="noopener noreferrer"&gt;MCP security best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-produccion-seguridad-permisos-supply-chain/" rel="noopener noreferrer"&gt;MCP en producción: seguridad, permisos y supply chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-outputschema-structuredcontent-agentes/" rel="noopener noreferrer"&gt;MCP outputSchema y structuredContent para agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/playwright-mcp-agentes-ia-testing-ui/" rel="noopener noreferrer"&gt;Playwright MCP para testing de UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/openai-agents-sdk-mcp-guardrails-tracing/" rel="noopener noreferrer"&gt;OpenAI Agents SDK: MCP, guardrails y tracing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Opus 4.1 retires in a week — and its replacement is exactly 3x cheaper</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Wed, 29 Jul 2026 11:52:42 +0000</pubDate>
      <link>https://dev.to/khavel/claude-opus-41-retires-in-a-week-and-its-replacement-is-exactly-3x-cheaper-5hah</link>
      <guid>https://dev.to/khavel/claude-opus-41-retires-in-a-week-and-its-replacement-is-exactly-3x-cheaper-5hah</guid>
      <description>&lt;p&gt;If you have &lt;code&gt;claude-opus-4-1-20250805&lt;/code&gt; hard-coded anywhere, you have until &lt;strong&gt;August 5, 2026&lt;/strong&gt;. That is seven days from the day this went up.&lt;/p&gt;

&lt;p&gt;Anthropic deprecated it on June 5, 2026 and published a tentative retirement date of August 5. It is the only Claude model on Anthropic's status table currently marked &lt;code&gt;Deprecated&lt;/code&gt; rather than &lt;code&gt;Active&lt;/code&gt; or &lt;code&gt;Retired&lt;/code&gt; — and the only Claude model with a retirement date still in the future.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the deadline. It's what the migration does to your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement is 3× cheaper on every single price dimension
&lt;/h2&gt;

&lt;p&gt;Anthropic's deprecation table names &lt;code&gt;claude-opus-4-8&lt;/code&gt; as the recommended replacement. Here is what changes, straight from Anthropic's published pricing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Price dimension (per MTok)&lt;/th&gt;
&lt;th&gt;Opus 4.1 (retiring)&lt;/th&gt;
&lt;th&gt;Opus 4.8 (replacement)&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache write (5 min)&lt;/td&gt;
&lt;td&gt;$18.75&lt;/td&gt;
&lt;td&gt;$6.25&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache write (1 hour)&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hits &amp;amp; refreshes&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch input&lt;/td&gt;
&lt;td&gt;$7.50&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch output&lt;/td&gt;
&lt;td&gt;$37.50&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;td&gt;3.0×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not "roughly a third". Exactly a third, on all seven, with no rounding.&lt;/p&gt;

&lt;p&gt;On a modest 10M input / 2M output month, that is &lt;strong&gt;$300 → $100&lt;/strong&gt;. On a coding-agent-shaped workload — call it 90M in and 25M out per month — it's &lt;strong&gt;$3,225 → $1,075&lt;/strong&gt;, a difference of $2,150 every month, for a migration you are being forced into anyway.&lt;/p&gt;

&lt;p&gt;And the price cut isn't a downgrade in disguise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Opus 4.1&lt;/th&gt;
&lt;th&gt;Opus 4.8&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;200k tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1M tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max output&lt;/td&gt;
&lt;td&gt;32k tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;128k tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five times the context, four times the max output, one third the price. It is rare for a forced migration to be strictly better on every published axis, and this one is.&lt;/p&gt;

&lt;h2&gt;
  
  
  One wrinkle worth knowing: the two docs pages name different targets
&lt;/h2&gt;

&lt;p&gt;Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/model-deprecations" rel="noopener noreferrer"&gt;model deprecations page&lt;/a&gt; lists the recommended replacement for &lt;code&gt;claude-opus-4-1-20250805&lt;/code&gt; as &lt;strong&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/strong&gt;, as a column in its migration table.&lt;/p&gt;

&lt;p&gt;Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/models/overview" rel="noopener noreferrer"&gt;models overview&lt;/a&gt; says, in prose, to migrate to &lt;strong&gt;Claude Opus 5&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both are first-party, and they are not really in conflict — Opus 5 is also $5 / $25 with a 1M context window, so the 3× arithmetic holds whichever you pick. But if you're automating a migration off a deprecation feed, this is the kind of thing that decides which string your script writes. Worth reading both pages rather than one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing: at Anthropic, "retired" is a per-cloud fact
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect to find while checking the above.&lt;/p&gt;

&lt;p&gt;Anthropic's status table lists five retired Claude models. But its &lt;strong&gt;live pricing table still carries three of them&lt;/strong&gt;, with full current rates and an inline qualifier naming the platforms where they're still served:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4&lt;/strong&gt; — &lt;em&gt;"retired, except on Google Cloud"&lt;/em&gt; — $15 in / $75 out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Sonnet 4&lt;/strong&gt; — &lt;em&gt;"retired, except on Bedrock and Google Cloud"&lt;/em&gt; — $3 / $15&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Haiku 3.5&lt;/strong&gt; — &lt;em&gt;"retired, except on Bedrock and Google Cloud"&lt;/em&gt; — $0.80 / $4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other two retired models (Claude 3.7 Sonnet, Claude 3 Haiku) are gone from the pricing table entirely.&lt;/p&gt;

&lt;p&gt;So "retired" on Anthropic's own API does not necessarily mean unavailable to &lt;em&gt;you&lt;/em&gt;. If you call Claude through Bedrock or Vertex, a model that is dead on the first-party API may still be serving your traffic, at the same published price it always had. That is genuinely useful if you're on a partner cloud — and genuinely misleading if you read "retired" as a single global fact and assume your Bedrock integration is about to break.&lt;/p&gt;

&lt;p&gt;Note the exception is stated &lt;strong&gt;per model&lt;/strong&gt;, not as a policy. Opus 4 is Google Cloud only; Sonnet 4 and Haiku 3.5 are Bedrock &lt;em&gt;and&lt;/em&gt; Google Cloud. "Retired means it's still on Bedrock" is false as a general rule — you have to read the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking this from code instead of from a docs page
&lt;/h2&gt;

&lt;p&gt;The reason I can put exact dates and per-cloud qualifiers in a table is that lifecycle status is a field I track, not prose I skimmed. It's a free JSON endpoint, no key, no signup, CORS open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# every Anthropic model with a retirement date still ahead of us&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://aimodelwatch.dev/api/deprecations.json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.models[] | select(.provider=="Anthropic" and .retires_on &amp;gt; "2026-07-29")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Claude Opus 4.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deprecated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deprecated_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retires_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"replacement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"api_string"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-1-20250805"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://platform.claude.com/docs/en/about-claude/model-deprecations"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row carries the &lt;code&gt;source_url&lt;/code&gt; it was read from, and fields the provider doesn't publish stay &lt;code&gt;null&lt;/code&gt; rather than getting filled in with a guess. Seventy-seven models across all providers currently carry declared lifecycle data there.&lt;/p&gt;

&lt;p&gt;The obvious use is a CI check: fail the build — or just warn — when a model ID in your config has a &lt;code&gt;retires_on&lt;/code&gt; inside your next release window. Vendors do email you about deprecations, but they email the account owner, not the repo, and the email doesn't know which of your services still pins the old string.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full deprecation table across providers: &lt;a href="https://aimodelwatch.dev/deprecations" rel="noopener noreferrer"&gt;https://aimodelwatch.dev/deprecations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JSON feed: &lt;a href="https://aimodelwatch.dev/api/deprecations.json" rel="noopener noreferrer"&gt;https://aimodelwatch.dev/api/deprecations.json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All prices and dates above were read on 2026-07-29 from Anthropic's own docs (&lt;code&gt;platform.claude.com/docs/en/about-claude/pricing&lt;/code&gt; and &lt;code&gt;/model-deprecations&lt;/code&gt;) and re-derived to the cent. Retirement dates are Anthropic's stated "tentative retirement date".&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>OpenAI Realtime API con WebRTC: cómo crear agentes de voz sin filtrar claves ni disparar costes</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Sat, 25 Jul 2026 10:09:43 +0000</pubDate>
      <link>https://dev.to/khavel/openai-realtime-api-con-webrtc-como-crear-agentes-de-voz-sin-filtrar-claves-ni-disparar-costes-3i5c</link>
      <guid>https://dev.to/khavel/openai-realtime-api-con-webrtc-como-crear-agentes-de-voz-sin-filtrar-claves-ni-disparar-costes-3i5c</guid>
      <description>&lt;p&gt;Un agente de voz en tiempo real no es solo streaming de audio. Necesita una frontera clara entre navegador, backend, Realtime API, tools, permisos, VAD, logs y costes para no convertirse en una demo peligrosa.&lt;/p&gt;

&lt;p&gt;OpenAI Realtime API con WebRTC permite crear agentes de voz de baja latencia donde el navegador envía y recibe audio por una conexión WebRTC, mientras un backend confiable inicializa la sesión, protege la API key real y define tools, permisos, logs y presupuesto.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;OpenAI Realtime API WebRTC&lt;/code&gt;. La intención de búsqueda en español es práctica: montar una arquitectura de voz en navegador sin exponer claves, entender cuándo usar tokens efímeros o interfaz unificada, y saber qué controles hacen falta antes de producción.&lt;/p&gt;

&lt;p&gt;Mi postura: no empieces por una demo con micro abierto y tools conectadas. Empieza por el límite de confianza. Si no sabes quién crea la sesión, quién ejecuta tools, qué se registra, cuánto cuesta cada minuto y qué acciones requieren aprobación, todavía no tienes un agente de voz: tienes un socket caro con permisos ambiguos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Qué es Realtime API con WebRTC
&lt;/h3&gt;

&lt;p&gt;Realtime API mantiene una sesión abierta para enviar audio, recibir eventos, actualizar estado y dejar que el modelo responda mientras la conversación sigue viva. WebRTC es la vía recomendada para experiencias de voz en navegador porque mueve audio en tiempo real con menos fricción que intentar hacer streaming manual desde JavaScript.&lt;/p&gt;

&lt;p&gt;La diferencia frente a un chatbot normal es importante. En chat puedes tolerar segundos de latencia, reintentos visibles y respuestas largas. En voz, 700 ms extra se sienten como interrupción, una tool lenta rompe el turno y una respuesta prolija parece mala UX aunque sea correcta.&lt;/p&gt;

&lt;p&gt;Para developers, la arquitectura mental correcta es esta: el navegador captura audio y reproduce audio; el backend crea o negocia la sesión; Realtime API gestiona el modelo y eventos; tus sistemas internos ejecutan acciones con permisos mínimos; observabilidad y costes se miden por sesión, turno y tool call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpkb54l8ribg2qmad75o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpkb54l8ribg2qmad75o.png" alt="Diagrama de agente de voz con navegador, backend que emite token efímero, conexión WebRTC, canal de datos, modelo realtime, tools, guardrails y registro de costes" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La frontera clave no es el audio: es separar cliente, backend confiable, sesión realtime, tools internas y controles de seguridad. El navegador nunca debería llevar la API key real.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Arquitectura recomendada para navegador
&lt;/h3&gt;

&lt;p&gt;En una app web, el navegador no debe contener una API key estándar. Debe pedir a tu backend una sesión o un token de vida corta. Ese backend autentica al usuario, aplica rate limit, define configuración inicial, adjunta un identificador de seguridad si procede y llama a la API de OpenAI con la clave real.&lt;/p&gt;

&lt;p&gt;OpenAI documenta dos formas de iniciar WebRTC desde cliente: una interfaz unificada donde el backend crea la llamada con &lt;code&gt;/v1/realtime/calls&lt;/code&gt;, y el patrón de token efímero donde el backend emite una credencial temporal y el navegador completa la negociación SDP con Realtime API. La elección depende de cuánto quieras poner al backend en el camino crítico de arranque.&lt;/p&gt;

&lt;p&gt;Yo usaría interfaz unificada si quieres control fuerte de sesión, auditoría centralizada y menos lógica sensible en cliente. Usaría token efímero cuando necesitas que el navegador conecte directamente, siempre con TTL corto, rate limit por usuario y configuración cerrada desde servidor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Flujo paso a paso
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1. El usuario abre la UI y concede permisos de micrófono. La app todavía no llama a tools ni abre una sesión privilegiada.&lt;/li&gt;
&lt;li&gt;2. El cliente pide a tu backend crear una sesión realtime. El backend autentica al usuario, decide modelo, voz, VAD, herramientas permitidas y presupuesto máximo.&lt;/li&gt;
&lt;li&gt;3. El navegador crea un &lt;code&gt;RTCPeerConnection&lt;/code&gt;, añade el track de audio local y prepara un canal de datos para eventos.&lt;/li&gt;
&lt;li&gt;4. La SDP offer viaja al backend o a Realtime API según el patrón elegido. La respuesta SDP queda como remote description y la sesión empieza.&lt;/li&gt;
&lt;li&gt;5. El audio de entrada fluye por WebRTC. El modelo devuelve audio, transcripción, eventos de respuesta y posibles tool calls.&lt;/li&gt;
&lt;li&gt;6. Las acciones sensibles pasan por tu servidor o por un MCP remoto con superficie limitada y aprobación. El resultado vuelve a la sesión como output de tool.&lt;/li&gt;
&lt;li&gt;7. Al cerrar, guardas métricas: duración, tokens de audio/texto, tool calls, errores, VAD, interrupciones, coste estimado y si hubo aprobación humana.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Código mínimo: backend Node para iniciar sesión
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;server.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/sdp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/realtime/call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requireUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enforceRealtimeQuota&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sdp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;realtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-realtime-2.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;turn_detection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;semantic_vad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;eagerness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;interrupt_response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Eres un asistente tecnico de soporte.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Responde breve en voz.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Confirma antes de ejecutar acciones con impacto externo.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No repitas secretos, tokens ni datos personales.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lookup_ticket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Busca un ticket permitido para el usuario autenticado&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ticket_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1/realtime/calls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OpenAI-Safety-Identifier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hashUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/sdp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ¿Te está sirviendo? Hay una dosis cada semana
&lt;/h3&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Este ejemplo deja la API key en servidor, aplica autenticación antes de crear sesión y evita que el cliente decida tools o presupuesto. En producción añadiría CORS estricto, CSRF si aplica, logs por sesión, límites por minuto, cierre explícito de sesiones abandonadas y una lista de tools por rol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Código mínimo: cliente WebRTC
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;client.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio#assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ontrack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracks&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTrack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;oai-events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;response.done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;recordTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;queueToolReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/realtime/call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/sdp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sdp&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRemoteDescription&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;answer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sdp&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cliente debe ser aburrido: capturar audio, negociar WebRTC, reproducir audio y mostrar estado. No debería decidir scopes, modelo caro, credenciales ni tools disponibles. Si necesitas cambiar permisos durante la sesión, hazlo desde servidor con una política verificable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools, MCP y acciones: dónde poner el límite
&lt;/h3&gt;

&lt;p&gt;Realtime puede trabajar con function tools, MCP remoto y conectores. La tentación es conectar CRM, calendario, base de datos y ticketing desde el primer día. Mala idea. Voz reduce la fricción de pedir acciones, así que también reduce el tiempo que tiene el usuario para revisar qué está autorizando.&lt;/p&gt;

&lt;p&gt;Para function tools, prefiero que tu aplicación ejecute la lógica y devuelva &lt;code&gt;function\_call\_output&lt;/code&gt;. Eso te permite aplicar permisos reales, validar argumentos, registrar payloads y pedir aprobación humana antes de mutaciones. Para MCP remoto, limita &lt;code&gt;allowed\_tools&lt;/code&gt; y asume que cualquier dato enviado en una tool call puede ser visto por ese servidor.&lt;/p&gt;

&lt;p&gt;La regla operativa: lectura con datos no sensibles puede ser automática; escritura, compra, envío, borrado, cambio de permisos o acceso a datos personales debe tener confirmación visible. En voz, la confirmación debe ser corta pero concreta: acción, destino, identificador y consecuencia.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  VAD, interrupciones y experiencia de conversación
&lt;/h3&gt;

&lt;p&gt;Voice Activity Detection decide cuándo empieza y termina el turno del usuario. Si cortas pronto, el agente responde antes de entender. Si esperas demasiado, parece lento. OpenAI documenta &lt;code&gt;server\_vad&lt;/code&gt; y &lt;code&gt;semantic\_vad&lt;/code&gt;; este último intenta trocear cuando el modelo cree que el usuario terminó la idea, no solo por silencio.&lt;/p&gt;

&lt;p&gt;Para soporte técnico, empezaría con &lt;code&gt;semantic\_vad&lt;/code&gt; y &lt;code&gt;interrupt\_response: true&lt;/code&gt;. Los usuarios interrumpen, corrigen IDs y cambian de objetivo. Si el agente no sabe parar, la experiencia parece una locución, no una conversación.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Mide interrupciones como métrica de producto. Muchas interrupciones pueden indicar que el agente habla demasiado, tarda en reconocer el objetivo o usa preambles molestos. No arregles eso solo subiendo modelo: muchas veces se corrige con prompts más claros y respuestas más cortas.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Prompting para voz: menos literatura, más política
&lt;/h3&gt;

&lt;p&gt;Un prompt de voz necesita estructura. Define rol, idioma, tono, longitud, cuándo usar tools, cuándo pedir datos, cuándo confirmar y cuándo escalar. &lt;code&gt;Sé útil y conciso&lt;/code&gt; no basta porque no dice qué hacer ante un número de pedido ambiguo, una tool lenta o una petición de borrar datos.&lt;/p&gt;

&lt;p&gt;Con modelos realtime con razonamiento, empieza con &lt;code&gt;reasoning.effort&lt;/code&gt; bajo y sube solo si hay tareas que realmente lo necesitan. La voz castiga la latencia. Prefiero un agente que resuelva el 80% de casos simples rápido y escale el resto, antes que uno que piense demasiado en cada saludo.&lt;/p&gt;

&lt;p&gt;Los preambles son útiles si son breves: &lt;code&gt;Lo reviso ahora&lt;/code&gt; antes de una tool lenta puede mejorar percepción. Pero si el agente rellena cada turno con frases de transición, estás pagando tokens para molestar. Define cuándo hablar mientras trabaja y cuándo quedarse callado.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Costes: lo que debes registrar desde el día uno
&lt;/h2&gt;

&lt;p&gt;El coste de voz no se parece al coste de un prompt textual aislado. Hay audio de entrada, audio de salida, texto, posibles tokens cacheados, tools, reintentos y sesiones largas. Además, una mala UX puede duplicar coste si el usuario repite porque el agente lo interrumpió o contestó tarde.&lt;/p&gt;

&lt;p&gt;Registra por sesión: modelo, duración, tokens por modalidad, respuestas canceladas, interrupciones, errores de tool, número de turns, coste estimado y usuario o tenant. No guardes audio completo por defecto salvo que tengas base legal y política clara; muchas veces bastan transcripciones redaccionadas y métricas agregadas.&lt;/p&gt;

&lt;p&gt;Realtime soporta prompt caching de forma automática cuando hay coincidencia de tokens entre respuestas, pero no lo trates como garantía de presupuesto. Diseña prompts estables, no metas contexto variable enorme al inicio y resume estado largo si la sesión se alarga.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seguridad y privacidad específicas de voz
&lt;/h2&gt;

&lt;p&gt;La voz introduce riesgos distintos. Puede contener datos personales que el usuario dice sin pensar, ruido de fondo, nombres de terceros o instrucciones inyectadas por otra persona cerca del micrófono. El agente no debería aceptar una orden sensible solo porque la oyó.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Añade controles simples: autenticación antes de sesión, scopes por usuario, denylist de datos que no se leen en voz, confirmación para acciones externas, timeouts, cierre al cambiar de pestaña si procede, y logs que no creen otra fuga. Para equipos regulados, separa entorno de demo y producción desde el primer prototipo.&lt;/p&gt;

&lt;p&gt;La prompt injection indirecta también aplica. Si el agente lee una web, ticket o documento y luego actúa, ese contenido debe tratarse como dato no confiable. Una frase dentro de un ticket no puede autorizar que el agente mande un email, borre un registro o exponga un secreto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuándo usar Agents SDK y cuándo ir directo a Realtime
&lt;/h3&gt;

&lt;p&gt;Si solo necesitas una UI web de voz con una o dos tools, ir directo a Realtime API con WebRTC puede ser más claro. Controlas la negociación, ves los eventos y entiendes bien la frontera cliente-servidor.&lt;/p&gt;

&lt;p&gt;Si necesitas handoffs, guardrails, especialistas, sesiones server-side, aprobación o integraciones complejas, mira la capa realtime del Agents SDK. La documentación describe &lt;code&gt;RealtimeAgent&lt;/code&gt;, &lt;code&gt;RealtimeRunner&lt;/code&gt;, &lt;code&gt;RealtimeSession&lt;/code&gt;, handoffs y guardrails específicos para respuestas y function-tool calls.&lt;/p&gt;

&lt;p&gt;No lo conviertas en religión de SDK. La pregunta buena es quién orquesta. Si el navegador solo captura audio, tu backend gestiona permisos y el SDK te ayuda a coordinar especialistas, tiene sentido. Si solo añade abstracción antes de entender el flujo, espera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API key estándar solo en servidor, nunca en navegador.&lt;/li&gt;
&lt;li&gt;Sesiones creadas tras autenticar usuario y aplicar cuota.&lt;/li&gt;
&lt;li&gt;Modelo, voz, VAD, tools y presupuesto definidos en backend.&lt;/li&gt;
&lt;li&gt;Tools separadas por rol, tenant y tipo de acción.&lt;/li&gt;
&lt;li&gt;Confirmación explícita para operaciones irreversibles o externas.&lt;/li&gt;
&lt;li&gt;Logs con IDs, métricas y errores; audio bruto solo si hay necesidad real y política.&lt;/li&gt;
&lt;li&gt;Evals de conversación con interrupciones, ruido, IDs, acentos y peticiones ambiguas.&lt;/li&gt;
&lt;li&gt;Monitor de coste por sesión y alertas por duración o reintentos.&lt;/li&gt;
&lt;li&gt;Fallback textual o humano si falla WebRTC, tool crítica o guardrail.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Conclusión
&lt;/h3&gt;

&lt;p&gt;OpenAI Realtime API con WebRTC ya permite construir agentes de voz muy convincentes, pero la parte difícil no es abrir el micrófono. La parte difícil es hacer que esa conversación tenga permisos, límites, coste predecible y una experiencia que no se rompa cuando el usuario interrumpe.&lt;/p&gt;

&lt;p&gt;Mi recomendación: construye primero el esqueleto de confianza. Backend que crea sesiones, cliente tonto, tools estrechas, VAD medido, confirmaciones visibles y coste por sesión. Después mejora voces, handoffs y prompts. Si lo haces al revés, tendrás una demo brillante y una deuda de seguridad desde el primer commit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es OpenAI Realtime API con WebRTC?
&lt;/h3&gt;

&lt;p&gt;Es una forma de conectar una app de navegador a modelos realtime mediante WebRTC para enviar audio, recibir audio y manejar eventos de conversación o tools con baja latencia.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo usar mi API key de OpenAI en el navegador?
&lt;/h3&gt;

&lt;p&gt;No deberías. La clave estándar debe quedarse en servidor. El navegador debe usar una sesión creada por backend o una credencial efímera de vida corta.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué diferencia hay entre WebRTC y WebSocket en Realtime API?
&lt;/h3&gt;

&lt;p&gt;WebRTC encaja mejor para audio directo desde navegador. WebSocket suele tener más sentido en pipelines server-side, telephony o cuando tu servidor controla el flujo de audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Realtime API puede llamar tools o MCP?
&lt;/h3&gt;

&lt;p&gt;Sí. Puede usar function tools, MCP remoto y conectores, pero las acciones sensibles necesitan permisos estrechos, validación y aprobación cuando haya impacto externo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo controlo el coste de un agente de voz?
&lt;/h3&gt;

&lt;p&gt;Mide duración, tokens de audio y texto, turns, reintentos, tools, respuestas canceladas y coste estimado por sesión. Añade cuotas por usuario o tenant desde el backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo usar Agents SDK para agentes de voz?
&lt;/h3&gt;

&lt;p&gt;Úsalo cuando necesites handoffs, guardrails, orquestación server-side o especialistas. Para una UI web simple, Realtime API directo puede ser más transparente al principio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo lanzar un agente de voz con OpenAI Realtime API y WebRTC sin abrir demasiado el sistema
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Definir caso de uso.&lt;/strong&gt; Elige una tarea de voz acotada, con datos permitidos y acciones claras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diseñar frontera de confianza.&lt;/strong&gt; Decide qué vive en navegador, backend, Realtime API y sistemas internos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crear endpoint de sesión.&lt;/strong&gt; Autentica usuario, aplica cuota y crea la sesión con API key solo en servidor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conectar WebRTC.&lt;/strong&gt; Captura micrófono, negocia SDP, reproduce audio y escucha eventos por data channel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Añadir tools mínimas.&lt;/strong&gt; Empieza por lectura segura y valida argumentos antes de ejecutar negocio real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configurar VAD.&lt;/strong&gt; Prueba &lt;code&gt;server\_vad&lt;/code&gt; y &lt;code&gt;semantic\_vad&lt;/code&gt;, mide interrupciones y latencia percibida.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrumentar coste.&lt;/strong&gt; Registra duración, tokens, tools, errores, reintentos y coste estimado por sesión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meter guardrails.&lt;/strong&gt; Bloquea datos sensibles, acciones no autorizadas y contenido externo que intente cambiar instrucciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probar con conversaciones reales.&lt;/strong&gt; Incluye ruido, acentos, IDs dictados, interrupciones y peticiones ambiguas antes de producción.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime-webrtc" rel="noopener noreferrer"&gt;OpenAI Realtime API with WebRTC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime" rel="noopener noreferrer"&gt;OpenAI Realtime and audio overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime-mcp" rel="noopener noreferrer"&gt;OpenAI Realtime with tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime-vad" rel="noopener noreferrer"&gt;OpenAI Realtime voice activity detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime-costs" rel="noopener noreferrer"&gt;OpenAI Realtime managing costs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/api/docs/guides/realtime-models-prompting" rel="noopener noreferrer"&gt;OpenAI Realtime prompting guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.github.io/openai-agents-python/realtime/guide/" rel="noopener noreferrer"&gt;OpenAI Agents SDK realtime guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openai/openai-realtime-agents" rel="noopener noreferrer"&gt;openai-realtime-agents demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/openai-agents-sdk-mcp-guardrails-tracing/" rel="noopener noreferrer"&gt;OpenAI Agents SDK: MCP, guardrails y tracing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para agentes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/litellm-proxy-gateway-llm-costes/" rel="noopener noreferrer"&gt;LiteLLM Proxy: gateway IA, costes y modelos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devaisemanal.com/mcp-outputschema-structuredcontent-agentes/" rel="noopener noreferrer"&gt;MCP outputSchema y structuredContent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recibe una lectura semanal de herramientas IA para devs
&lt;/h3&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
    <item>
      <title>Búsqueda híbrida RAG: BM25, vectores y reranking sin complicar tu stack</title>
      <dc:creator>Khavel</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:42:42 +0000</pubDate>
      <link>https://dev.to/khavel/busqueda-hibrida-rag-bm25-vectores-y-reranking-sin-complicar-tu-stack-ajn</link>
      <guid>https://dev.to/khavel/busqueda-hibrida-rag-bm25-vectores-y-reranking-sin-complicar-tu-stack-ajn</guid>
      <description>&lt;p&gt;La búsqueda vectorial pura falla justo en consultas con IDs, nombres propios y términos raros. La búsqueda híbrida RAG combina BM25, embeddings y reranking para recuperar mejor evidencia antes de llamar al modelo.&lt;/p&gt;

&lt;p&gt;Búsqueda híbrida RAG significa ejecutar recuperación léxica, normalmente BM25 o full-text search, junto a recuperación semántica por embeddings, fusionar rankings y pasar al LLM un contexto ordenado con evidencias citables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arquitectura base&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;La keyword principal es &lt;code&gt;búsqueda híbrida RAG&lt;/code&gt;. La intención de búsqueda en español es práctica: entender cuándo la búsqueda vectorial se queda corta, cómo combinar BM25 con vectores y cómo evaluar si el cambio mejora respuestas reales.&lt;/p&gt;

&lt;p&gt;Mi postura: si tu RAG responde sobre documentación técnica, soporte, contratos, catálogos, logs o conocimiento interno con nombres propios, no deberías empezar por vector-only. Empieza híbrido o al menos deja el camino preparado para activarlo sin reindexar todo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Briefing&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Qué problema resuelve la búsqueda híbrida RAG
&lt;/h3&gt;

&lt;p&gt;La búsqueda vectorial es buena capturando significado. Si alguien pregunta &lt;code&gt;cómo revocar una clave&lt;/code&gt;, puede encontrar documentos que hablan de rotación, credenciales o secretos aunque no usen las mismas palabras. Ese es su valor.&lt;/p&gt;

&lt;p&gt;Pero los embeddings tropiezan con lo exacto: &lt;code&gt;ERR\_CONN\_RESET&lt;/code&gt;, &lt;code&gt;invoice\_2026\_041&lt;/code&gt;, &lt;code&gt;TenantIsolationPolicy&lt;/code&gt;, &lt;code&gt;SKU-A17&lt;/code&gt;, una clase interna o un endpoint raro. Para un humano esos tokens son la pista principal. Para un vector pueden quedar diluidos como ruido.&lt;/p&gt;

&lt;p&gt;BM25 y full-text search hacen lo contrario: premian coincidencias léxicas, frecuencia de términos y rareza de palabras. La búsqueda híbrida combina ambas señales para que el sistema no tenga que elegir entre significado y precisión.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0d7wtkb89ux032yzmhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0d7wtkb89ux032yzmhi.png" alt="Diagrama de búsqueda híbrida RAG con consulta, recuperación BM25, recuperación vectorial, fusión RRF, reranking y contexto citado para el modelo" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Una arquitectura híbrida separa recall léxico, recall semántico, fusión de rankings, reranking y evaluación. No mete más chunks por intuición: decide qué evidencia merece llegar al prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lectura práctica&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  La arquitectura mínima: dos recuperadores y una fusión
&lt;/h3&gt;

&lt;p&gt;El patrón base tiene cuatro pasos. Primero normalizas la consulta y aplicas permisos o filtros duros. Segundo ejecutas BM25 o full-text search contra el texto indexado. Tercero ejecutas búsqueda vectorial contra embeddings de chunks. Cuarto fusionas ambos rankings con una regla estable, normalmente Reciprocal Rank Fusion cuando no quieres calibrar scores heterogéneos.&lt;/p&gt;

&lt;p&gt;La clave es no mezclar puntuaciones crudas sin pensar. Un score BM25 no significa lo mismo que una similitud coseno o producto interno. RRF evita parte del problema porque trabaja con posiciones de ranking, no con escalas absolutas. Si un documento aparece arriba en dos listas, sube. Si solo aparece en una, todavía puede entrar, pero con menos fuerza.&lt;/p&gt;

&lt;p&gt;Después puedes añadir reranking. Un cross-encoder o late-interaction reranker mira pares &lt;code&gt;consulta-documento&lt;/code&gt; con más detalle y reordena un conjunto pequeño de candidatos. Es más caro, así que suele aplicarse después de recuperar 40-100 candidatos, no sobre todo el corpus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuándo usar híbrida y cuándo no
&lt;/h3&gt;

&lt;p&gt;Usa búsqueda híbrida si tus usuarios preguntan con nombres exactos, errores, siglas, IDs, versiones, rutas, clases, productos, tickets o fragmentos copiados de una interfaz. Es el caso normal en RAG para developers y soporte técnico.&lt;/p&gt;

&lt;p&gt;También encaja cuando el corpus mezcla lenguaje natural con tablas, documentos largos, documentación API, changelogs, incidencias y preguntas con permisos. En esos entornos, el vector-only suele parecer convincente en demo y fallar en producción cuando aparece terminología específica.&lt;/p&gt;

&lt;p&gt;No la añadas por moda si tu corpus es pequeño, homogéneo y semánticamente simple. Si tienes 200 documentos y las consultas son abiertas, una búsqueda vectorial bien evaluada puede bastar. La regla pragmática es medir: si pierdes consultas exactas o tienes respuestas sin citas fuertes, híbrida deja de ser complejidad extra y pasa a ser higiene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Código: RRF simple para unir BM25 y vectores
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;rrf.py&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reciprocal_rank_fusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ranked_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rrf_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked_ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERR_CONN_RESET al refrescar token OAuth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;bm25_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bm25_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;vector_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vector_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reciprocal_rank_fusion&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bm25_hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_hits&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rerank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;])[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_with_citations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Puntos a revisar&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que conviene comprobar
&lt;/h3&gt;

&lt;p&gt;Este ejemplo no depende de un proveedor concreto. La idea es deliberadamente simple: recupera dos listas, fusiona por posición, rerankea pocos candidatos y genera solo con contexto citado. Después puedes sustituir &lt;code&gt;bm25\_search&lt;/code&gt;, &lt;code&gt;vector\_search&lt;/code&gt; y &lt;code&gt;rerank&lt;/code&gt; por PostgreSQL, Azure AI Search, Qdrant, Weaviate, Pinecone, Elasticsearch o tu stack actual.&lt;/p&gt;

&lt;p&gt;¿Te está sirviendo? Hay una dosis cada semana&lt;/p&gt;

&lt;p&gt;Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación con PostgreSQL y pgvector
&lt;/h2&gt;

&lt;p&gt;PostgreSQL es una opción muy razonable cuando tu corpus vive cerca de datos transaccionales, permisos por tenant o joins que no quieres duplicar en otro sistema. &lt;code&gt;tsvector&lt;/code&gt; y &lt;code&gt;tsquery&lt;/code&gt; cubren full-text search; pgvector añade almacenamiento y búsqueda de embeddings. Para muchos productos internos, esa combinación reduce sincronización y fugas entre sistemas.&lt;/p&gt;

&lt;p&gt;El diseño típico guarda &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;tenant\_id&lt;/code&gt;, &lt;code&gt;tsv&lt;/code&gt; y &lt;code&gt;embedding&lt;/code&gt; en la misma tabla. La query aplica primero filtros obligatorios, ejecuta full-text y vector search por separado, calcula posiciones y fusiona con RRF en SQL o en aplicación. Lo importante es que los permisos no sean un filtro posterior decorativo: deben aplicarse antes de recuperar candidatos.&lt;/p&gt;

&lt;p&gt;Postgres no siempre será el buscador más rápido para corpus enormes o requisitos avanzados de relevancia. Pero como baseline operable es fuerte: transacciones, backups, permisos, SQL, joins y menos piezas móviles. Si el equipo no puede operar dos índices con disciplina, una arquitectura más simple puede ganar aunque no sea la más glamourosa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lectura práctica&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementación con motores dedicados
&lt;/h3&gt;

&lt;p&gt;Azure AI Search documenta híbrida como ejecución paralela de full-text y vector queries, con RRF para devolver un único ranking. Es una buena lectura porque separa claramente BM25, HNSW/eKNN y fusión.&lt;/p&gt;

&lt;p&gt;Weaviate expone búsqueda híbrida con BM25F y vector search, configurable por peso y método de fusión. Qdrant permite consultas híbridas con vectores densos, sparse y reranking; su documentación reciente empuja un patrón de ingestión con embeddings densos, sparse y late-interaction. Pinecone soporta patrones sparse-dense y enfoques con índice híbrido o combinación de señales según el tipo de índice.&lt;/p&gt;

&lt;p&gt;La decisión no debería ser &lt;code&gt;qué vector database está de moda&lt;/code&gt;. Pregunta: dónde viven tus permisos, cómo vas a versionar embeddings, cómo filtrarás por tenant, cómo depurarás un resultado malo, cuánto cuesta rerankear y quién operará el índice cuando falle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Briefing&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tuning: alpha, top_k y reranking
&lt;/h3&gt;

&lt;p&gt;Si tu proveedor ofrece un peso tipo &lt;code&gt;alpha&lt;/code&gt;, no lo trates como una constante universal. Queries con IDs suelen necesitar más señal léxica. Queries conceptuales suelen necesitar más señal semántica. Puedes empezar con un valor medio, pero guarda métricas por tipo de consulta.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;top\_k&lt;/code&gt; antes de fusionar y después de rerankear importa más de lo que parece. Si recuperas pocos candidatos, el documento correcto ni llega al reranker. Si recuperas demasiados, suben coste, latencia y ruido. Una configuración común es recuperar 30-100 por canal, fusionar, rerankear 40-100 y pasar 5-15 chunks finales al LLM.&lt;/p&gt;

&lt;p&gt;El reranking solo compensa si el candidato correcto está en el pool. Si context recall es bajo, no arregles con un reranker caro. Arregla chunking, filtros, normalización de query, sinónimos, indexación de campos o combinación sparse/dense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluación: no publiques híbrida sin comparar contra baseline
&lt;/h2&gt;

&lt;p&gt;Antes de activar búsqueda híbrida, congela un dataset pequeño: preguntas reales, documentos esperados cuando existan, categoría de query y riesgo. Ejecuta vector-only, BM25-only e híbrida con el mismo corpus. Mide recall@k, MRR, nDCG si tienes qrels, groundedness de respuesta y coste por consulta.&lt;/p&gt;

&lt;p&gt;La mejora que busco no es solo más score agregado. Quiero ver casos concretos: errores exactos que BM25 rescata, preguntas conceptuales que el vector mantiene, documentos irrelevantes que el reranker expulsa y respuestas que citan mejor evidencia.&lt;/p&gt;

&lt;p&gt;No cambies embeddings, chunking, prompt, reranker y fusión en el mismo experimento. Si lo haces, no sabrás qué ayudó. La búsqueda híbrida es un cambio suficientemente grande como para merecer baseline propio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lectura práctica&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Seguridad, permisos y privacidad
&lt;/h3&gt;

&lt;p&gt;El fallo peligroso en RAG no es solo responder mal. Es recuperar un documento correcto para el usuario equivocado. En híbrida hay más caminos para que un documento entre al candidate pool, así que los filtros de tenant, permisos, clasificación y fecha deben aplicarse antes de ranking o en cada subconsulta.&lt;/p&gt;

&lt;p&gt;Evita indexar secretos, claves, dumps, prompts internos sensibles o datos personales que no necesites para la tarea. Si el corpus incluye contenido no confiable, como tickets, emails, páginas externas o docs subidas por usuarios, trata esos chunks como datos, no como instrucciones. Esto conecta directamente con defensas contra prompt injection indirecta.&lt;/p&gt;

&lt;p&gt;Para observabilidad, registra IDs de documentos, scores, rankings, filtros aplicados y versión de índice. No necesitas guardar todo el texto recuperado en logs permanentes. Muchas veces basta con referencias y muestras controladas para depurar sin crear otra base de datos sensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes que veo en RAG híbrido
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fusionar scores BM25 y vectoriales como si estuvieran en la misma escala.&lt;/li&gt;
&lt;li&gt;Aplicar filtros de permisos después de recuperar, cuando el ranking ya fue contaminado.&lt;/li&gt;
&lt;li&gt;Usar &lt;code&gt;top\_k&lt;/code&gt; pequeño y culpar al reranker de no encontrar documentos que nunca recibió.&lt;/li&gt;
&lt;li&gt;Indexar chunks sin títulos, rutas, fechas, producto, versión o metadatos útiles para desempatar.&lt;/li&gt;
&lt;li&gt;No separar consultas exactas, conceptuales, negativas y multi-hop en la evaluación.&lt;/li&gt;
&lt;li&gt;Medir solo la respuesta final y no guardar los candidatos que llegaron al prompt.&lt;/li&gt;
&lt;li&gt;Añadir híbrida para tapar un problema de chunking obvio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Plan de adopción en una semana
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Día 1: etiqueta 40-60 preguntas reales y separa consultas con IDs, errores, nombres propios, conceptos generales, permisos y preguntas sin respuesta.&lt;/li&gt;
&lt;li&gt;Día 2: ejecuta tu pipeline vector-only y guarda candidatos, respuesta, citas, latencia y coste.&lt;/li&gt;
&lt;li&gt;Día 3: añade recuperación BM25 o full-text con los mismos filtros de permisos.&lt;/li&gt;
&lt;li&gt;Día 4: fusiona con RRF y compara candidate pools antes de tocar prompts.&lt;/li&gt;
&lt;li&gt;Día 5: añade reranking solo sobre candidatos fusionados y mide si mejora precisión sin romper latencia.&lt;/li&gt;
&lt;li&gt;Día 6: ajusta top_k por tipo de consulta y crea un gate mínimo de regression retrieval.&lt;/li&gt;
&lt;li&gt;Día 7: despliega para un porcentaje pequeño de tráfico y revisa ejemplos, no solo promedios.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;La búsqueda híbrida RAG no es una capa elegante para presumir de arquitectura. Es una corrección práctica a un defecto real de vector-only: confundir parecido semántico con evidencia suficiente.&lt;/p&gt;

&lt;p&gt;Mi recomendación es empezar por el pipeline más aburrido que puedas operar: filtros duros, BM25, vectores, RRF, reranking opcional, citas y evaluación. Si eso mejora recall y groundedness en preguntas reales, ya tendrás permiso técnico para invertir en motores más sofisticados. Si no lo mide, es solo otro índice caro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;p&gt;¿Qué es búsqueda híbrida RAG?&lt;/p&gt;

&lt;p&gt;Es un enfoque de recuperación para RAG que combina búsqueda léxica como BM25 o full-text search con búsqueda vectorial por embeddings, fusiona resultados y entrega al modelo un contexto más fiable.&lt;/p&gt;

&lt;p&gt;¿Por qué BM25 sigue siendo útil con embeddings?&lt;/p&gt;

&lt;p&gt;Porque BM25 captura coincidencias exactas, términos raros, IDs, errores, nombres propios y acrónimos que los embeddings pueden suavizar demasiado.&lt;/p&gt;

&lt;p&gt;¿Qué es RRF en búsqueda híbrida?&lt;/p&gt;

&lt;p&gt;Reciprocal Rank Fusion es una técnica para fusionar listas ordenadas usando la posición de cada documento en cada ranking, sin depender de que los scores tengan la misma escala.&lt;/p&gt;

&lt;p&gt;¿Necesito reranking en un RAG híbrido?&lt;/p&gt;

&lt;p&gt;No siempre. Añádelo cuando tengas suficientes candidatos, consultas ambiguas o requisitos altos de precisión. Primero mide si híbrida sin reranker ya resuelve el fallo.&lt;/p&gt;

&lt;p&gt;¿PostgreSQL con pgvector basta para búsqueda híbrida?&lt;/p&gt;

&lt;p&gt;Para muchos productos internos sí, especialmente si necesitas joins, permisos y transacciones cerca del corpus. Para escalas grandes o relevancia avanzada, puede convenir un motor dedicado.&lt;/p&gt;

&lt;p&gt;¿Cómo evalúo una búsqueda híbrida RAG?&lt;/p&gt;

&lt;p&gt;Compara BM25-only, vector-only e híbrida con preguntas reales. Mide recall@k, MRR o nDCG, groundedness, calidad de citas, latencia y coste por consulta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo implementar búsqueda híbrida RAG sin rehacer todo el sistema
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crear baseline.&lt;/strong&gt; Guarda preguntas reales, documentos esperados, candidatos vector-only, respuesta, citas, latencia y coste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Añadir índice léxico.&lt;/strong&gt; Indexa texto y metadatos con BM25, full-text search o sparse vectors sin saltarte permisos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecutar dos recuperadores.&lt;/strong&gt; Lanza búsqueda léxica y vectorial con la misma query normalizada y filtros obligatorios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fusionar rankings.&lt;/strong&gt; Usa RRF o una combinación calibrada; evita sumar scores crudos sin normalización.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rerankear candidatos.&lt;/strong&gt; Aplica reranking solo sobre el pool fusionado, no sobre todo el corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construir contexto final.&lt;/strong&gt; Deduplica chunks, conserva citas, limita ruido y ordena por utilidad para la respuesta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir regresiones.&lt;/strong&gt; Compara contra baseline con recall@k, MRR, groundedness, coste y ejemplos fallidos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desplegar gradualmente.&lt;/strong&gt; Activa por cohortes o tipos de consulta y revisa trazas antes de subir tráfico.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Cierre editorial&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Criterio técnico
&lt;/h3&gt;

&lt;p&gt;Un buen chunk en tiempo real no es el más corto ni el más semántico: es el que conserva evidencia, tiempo y estado suficiente para responder sin inventar continuidad.&lt;/p&gt;

&lt;p&gt;Fuentes y referencias&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/search/hybrid-search-overview" rel="noopener noreferrer"&gt;Azure AI Search: hybrid search overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking" rel="noopener noreferrer"&gt;Azure AI Search: RRF ranking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.weaviate.io/weaviate/search/hybrid" rel="noopener noreferrer"&gt;Weaviate: hybrid search documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qdrant.tech/documentation/tutorials-basics/reranking-hybrid-search/" rel="noopener noreferrer"&gt;Qdrant: hybrid search with reranking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qdrant.tech/documentation/search/hybrid-queries/" rel="noopener noreferrer"&gt;Qdrant: hybrid queries and RRF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pinecone.io/guides/search/hybrid-search" rel="noopener noreferrer"&gt;Pinecone: hybrid search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/textsearch-controls.html" rel="noopener noreferrer"&gt;PostgreSQL: controlling text search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector: vector similarity search for Postgres&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2210.11934" rel="noopener noreferrer"&gt;arXiv: An Analysis of Fusion Functions for Hybrid Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También te puede interesar&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/real-time-chunking-rag-streaming/" rel="noopener noreferrer"&gt;Real-time chunking para RAG y agentes&lt;/a&gt;&lt;a href="https://devaisemanal.com/evaluacion-rag-produccion-metricas-datasets/" rel="noopener noreferrer"&gt;Evaluación RAG en producción&lt;/a&gt;&lt;a href="https://devaisemanal.com/opentelemetry-genai-observabilidad-agentes/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI para agentes&lt;/a&gt;&lt;a href="https://devaisemanal.com/litellm-proxy-gateway-llm-costes/" rel="noopener noreferrer"&gt;LiteLLM Proxy: gateway IA, costes y modelos&lt;/a&gt;&lt;a href="https://devaisemanal.com/prompt-injection-agentes-ia-seguridad-evals/" rel="noopener noreferrer"&gt;Prompt injection en agentes de IA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recibe una lectura semanal de herramientas IA para devs&lt;/p&gt;

&lt;p&gt;Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devaisemanal.com/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=spanish_digest#/portal/signup" rel="noopener noreferrer"&gt;Suscribirme gratis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>ai</category>
      <category>espanol</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
