Mert
Samsung Galaxy S27 Pro Bazı Özellikleri Sızdırıldı
Stellantis, küçük ve erişilebilir yeni elektrikli araç projesini duyurdu
Çok sayıda otomobil devini çatısı altında barındıran Stellantis, bugün küçük ve erişilebilir yeni elektrikli araç projesini duyurdu. Bu konuda şirketten yapılan açıklamada, “Stellantis, yeni nesil projeleriyle ve yatırımlarıyla sürdürülebilir büyüme hamlesini desteklemeye devam ediyor. Bu kapsamda şirket çığır açan, küçük ve erişilebilir elektrikli araç projesi E-Car’ın hayata geçirildiğini duyurdu. İlk E-Car modellerinin 2028 yılında üretim hattından çıkması hedefleniyor. Stellantis, E-Car’daki “E” harfinin; European (Avrupalı), Emotion (Duygu), Electric (Elektrikli) ve Environmental friendliness (Çevre Dostu) kavramlarını temsil ettiğini açıkladı. Avrupa’da Avrupalılar için üretilecek bu yüksek potansiyelli segment, Avrupa Komisyonu tarafından; Avrupa’daki tasarım ve üretim istihdamını destekleme potansiyeli taşıması ve günlük şehir içi mobilitede tamamen elektrikli araçların daha geniş çapta benimsenmesine katkı sağlaması nedeniyle önemli bir proje olarak değerlendiriliyor. E-Car; Avrupa’daki küçük ve erişilebilir otomobil segmentinde son yıllarda yaşanan ciddi daralmaya çözüm sunmak amacıyla, Avrupa’nın “halk için mobilite” geleneğine sadık kalınarak geliştirilen; küçük, yenilikçi, erişilebilir ve tamamen elektrikli bir araç olarak öne çıkıyor. […]
Stellantis, küçük ve erişilebilir yeni elektrikli araç projesini duyurdu yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.
Technical SEO for Next.js: SSR, JSON-LD, and Sitemaps
I don’t offer SEO as a service. But every site I build ships with technical SEO baked in — because it’s not marketing, it’s engineering. A site that search engines can’t crawl or understand is a site that doesn’t work properly. In the pikkuna.fi e-commerce build, that meant correct hreflang across 30 languages, server-rendered product pages, and JSON-LD that survived localization. Same principles, scaled up.
slug=”mvp-development”
text=”Building a Next.js project that needs to rank from day one? SSR, structured data, and sitemaps are part of what I ship — not add-ons.”
/>
Server-Side Rendering by Default
Single-page apps with client-side rendering are invisible to most crawlers. Every project I build uses Next.js App Router with server components:
// app/products/[slug]/page.tsx
export default async function ProductPage({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params;
const product = await getProduct(slug);
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
</article>
);
}
The HTML arrives fully rendered. No JavaScript required for indexing.
Dynamic Meta Tags
Every page gets proper meta tags generated from actual content:
// app/products/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const product = await getProduct(slug);
return {
title: `${product.name} | Store`,
description: product.description.slice(0, 160),
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
};
}
This handles SEO meta tags, Open Graph for social sharing, and Twitter cards — all from one function.
JSON-LD Structured Data
Schema.org markup tells search engines exactly what the content represents:
function ProductJsonLd({ product }: { product: Product }) {
const schema = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.image,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "EUR",
availability: "https://schema.org/InStock",
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
I add appropriate schemas based on content type: Product, Article, Organization, FAQPage, BreadcrumbList.
Automatic Sitemap Generation
Next.js can generate sitemaps from your data:
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const products = await getAllProducts();
return [
{ url: "https://example.com", lastModified: new Date() },
...products.map((p) => ({
url: `https://example.com/products/${p.slug}`,
lastModified: p.updatedAt,
})),
];
}
No plugins, no external services. Just code that generates /sitemap.xml at build time.
This isn’t optimization work you hire someone for later. It’s the baseline for a properly built website. When I deliver a project, the technical SEO foundation is already there — because skipping it means shipping broken software.
If you want to go further with the indexing layer, read IndexNow in Next.js: Instant Indexing After Every Deploy and llms.txt for AI Discoverability. Together with the patterns above, that’s the full stack.
slug=”seo-audit”
text=”Want this same checklist applied to your existing site? Fixed-fee Technical SEO Audit — keyword research per market, schema, sitemap, hreflang, Core Web Vitals, indexing, broken links, OG images — delivered as a prioritised written report in 5 working days.”
/>
For MVP development with this foundation built in from the start — get in touch.
I built nvm-style account manager for Claude Code
If you use Claude Code with more than one account – personal and work, or one per client – you know the dance. claude opens, you realize you’re under the wrong account, you /logout, /login, sit through the OAuth browser flow, then forget which account you’re in by the time you switch projects again.
I built claudenv to make this stop happening.
claudenv use work
claude # automatically under your work account
cd ~/projects/clientA
# auto-switches if you opted in, or:
claudenv use personal
That’s it. The same claude binary, just pointed at different config directories via the official CLAUDE_CONFIG_DIR environment variable. No symlinks, no patching files in place, no daemons.
How it works
Claude Code reads its config from ~/.claude by default, but respects CLAUDE_CONFIG_DIR if set. claudenv keeps one directory per account under ~/.claudenv/accounts/<name>/ and points the env var at the right one. Each account has its own OAuth state, settings, MCP servers, the whole thing.
There are three layers of state, exactly like nvm:
-
Global default – what you set with
claudenv use work, persists across shells -
Per-project override – a
.claudenvrcfile in your repo, picked up via walk-up resolution like.nvmrc -
Shell-session override –
.claudenvrcauto-switches oncdif you enable the hook
Set a default, override per-project, forget about it.
Why not just claude-switch or claudectx?
There are other tools in this space – I want to be upfront about that.
claude-switch uses the same CLAUDE_CONFIG_DIR mechanism, but the workflow is per-launch: claude-switch work runs Claude under that profile, every time you start a session. If you prefer being explicit on every invocation, use it – it’s a fine tool.
claudectx and a few others go a different route entirely, symlinking or patching ~/.claude.json in place. That works too, but it’s stateful in a way I didn’t want. If the symlink breaks or a patch fails halfway, you end up in a weird state that’s hard to debug.
claudenv is for people who switch accounts more than once a day and don’t want to type the profile name every time. If your projects are clearly tied to specific accounts, .claudenvrc + auto-switch means you basically never think about it again.
Install
curl -fsSL https://raw.githubusercontent.com/bodasooqa/claudenv/main/install.sh | bash
Or with wget, or download and inspect first – instructions are in the README.
Bash or zsh, that’s the only requirement. No Node, no Python, no Homebrew, no extra dependencies. Twenty lines into ~/.zshrc, you’re done.
What’s next
A few things on the roadmap: a claudenv self-update command, a Homebrew tap once there’s a real release cadence, and a wrapper that makes GUI-launched IDEs (Cursor, VS Code from the Dock or Spotlight) pick up the active profile – currently that’s a known limitation with launchd not reading shell rc files, documented in the README.
If it solves your problem, a star helps me prioritize what to build next. Issues and PRs welcome.

bodasooqa
/
claudenv
nvm-style account manager for Claude Code. Switch profiles via CLAUDE_CONFIG_DIR, with per-project .claudenvrc and auto-switch on cd.
claudenv
An nvm-style account manager for Claude Code. Set a default account, override per-project with .claudenvrc, optionally auto-switch on cd. Zero dependencies.
claudenv import default # save current ~/.claude as 'default' (auto-activated — first profile)
claudenv add work # create new empty slot
claudenv use work # switch this shell + set as global default
claude # /login here, isolated from 'default'
When
accounts/is empty, the firstimportoraddauto-activates the new profile so you don’t have to follow withclaudenv use. Subsequent ones don’t change the active profile.
Is this for you?
Claude Code already supports CLAUDE_CONFIG_DIR for account isolation. Several tools wrap this — pick the one that fits how you work:
| claudenv | claude-switch | |
|---|---|---|
| Mental model | Set a default, work normally | Specify account on every launch |
| Typical command |
claude (after claudenv use work) |
claude-switch work |
| Per-project config |
.claudenvrc (walk-up resolution) |
✗ |
…
My Full Make.com Blueprint For Automating a Sheets OpenAI Ghost Content Pipeline
Why I stopped hand-assembling content
I got tired of copy pasting my way through content.
Idea in Notion. Outline in Docs. Draft in ChatGPT. Formatting in Ghost. Images in Figma. Every post felt like a manual QA job instead of building something once and letting it run.
So I pushed the whole thing into Make.com.
Now a single row in Google Sheets turns into a drafted or published Ghost post. Titles, meta fields, tags, even a first-pass excerpt. All wired together with OpenAI in the middle.
This is the exact blueprint. Not theory. This is the scenario that runs my real content pipeline.
The high-level architecture
The stack is simple on purpose:
- Google Sheets as the content control panel.
- Make.com to orchestrate everything.
- OpenAI to generate and clean up text.
- Ghost (self-hosted) as the CMS.
One sheet row = one post. Status and flags in that row decide if Make should ignore it, draft it, or publish it.
I keep the Make scenario focused. One job. Turn structured data in Sheets into structured content in Ghost.
The Google Sheet schema
Here is the actual column setup from my sheet. No fluff.
-
A: id– internal ID, just an incrementing number. -
B: status–idea,ready,generated,published. -
C: title– human-written title seed. -
D: slug– usually auto-generated in Make if empty. -
E: brief– 2–4 bullet outline or a short paragraph. -
F: target_keywords– comma separated SEO terms. -
G: tone– e.g.direct, informal, first-person. -
H: word_count_goal– rough length like1200. -
I: tags– comma separated Ghost tags. -
J: generated_html– where I store the body HTML from OpenAI. K: meta_titleL: meta_descriptionM: excerpt-
N: publish_flag–draftorpublish. -
O: ghost_post_id– for updates.
I do not trust LLMs to guess intent. So I give them a tight brief and flags.
The Make.com scenario: the main flow
I run a single Make scenario that does all of this:
- Polls Google Sheets for new
readyrows. - Generates content and meta fields via OpenAI.
- Writes the generated content back into the sheet.
- Creates or updates a Ghost post.
- Updates status and IDs.
I will walk through every module and what it actually does. No hand waving.
Module 1: Google Sheets “Search Rows”
Trigger: I run this scenario on a schedule. Every 15 minutes works for my pace.
First module is a Google Sheets > Search Rows module.
-
Spreadsheet: my
Content Pipelinesheet. -
Sheet:
Posts. -
Filter:
status = ready. - Limit: I cap at 3 rows per run. I prefer small batches.
This returns an array of rows that are ready to be turned into drafts.
Each row carries all the columns I mentioned earlier.
Module 2: Iterator
I drop an Iterator right after Search Rows.
No magic here. It splits the list of rows into separate bundles so I can process each post independently.
The iterator just takes the array of rows from Sheets and iterates over values[].
Module 3: Data cleanup with Text & Tools
I do some pre-processing before I hit OpenAI.
- Trim whitespace from things like
titleandslug. - Normalize commas in
tagsandtarget_keywords. - Generate a fallback slug if the cell is empty.
In practice this is a couple of Tools > Text aggregator / Text parser modules and one Set variable module.
Example: to generate the slug I use a simple Make formula in a Set variable module:
lowercase(
replace(
replace(
replace(
stripDiacritics(title);
" "; "-"
);
"--"; "-"
);
"[^a-z0-9-]"; ""
)
)
If the slug column in Sheets is already filled, I keep it. Otherwise this kicks in.
Module 4: OpenAI “Create a chat completion” for the article body
Now the fun part.
I use the OpenAI > Create a chat completion module. Currently on gpt-4.1 but I keep this modular so I can swap models.
I feed it a structured prompt that merges sheet data and my own style instructions. Here is the actual system + user content simplified to fit here.
System message (in the module):
You are Richard Lemon, a creative web experience developer.
Write opinionated first-person blog posts.
No em dashes. Short paragraphs (max 2-3 lines).
Direct voice, practical, specific.
Use HTML with <h2>, <p>, <ul>, <li>, <pre>.
Do not add <html> or <body> tags.
User message template:
Write a blog post with the following constraints:
Title: {{title}}
Brief: {{brief}}
Target keywords: {{target_keywords}}
Tone: {{tone}}
Word count target: {{word_count_goal}}
Rules:
- Write in first person as if I actually did the thing.
- Be concrete and technical where useful.
- Use only HTML tags for structure (<h2>, <p>, <ul>, <li>, <pre>, <code>).
- No introduction fluff, start with context or action.
- No conclusion heading.
Return only the HTML body.
The output is pure HTML. I do not want Markdown here because Ghost accepts HTML just fine and I like having precise control.
Module 5: OpenAI for SEO meta + excerpt
I split generation into two calls instead of asking one model response to do everything.
Second OpenAI > Create a chat completion module takes the title, target keywords, and a short summary of the generated HTML and creates meta fields plus an excerpt.
System message:
You create SEO metadata for blog posts.
Always stay under the specified character limits.
Return JSON only.
User message:
Post title: {{title}}
Target keywords: {{target_keywords}}
Body HTML (truncated): {{body_preview}}
Generate:
- meta_title: max 60 chars, must include at least one keyword.
- meta_description: 120-155 chars, natural and specific.
- excerpt: 1-2 sentences, no clickbait.
Return JSON like this:
{
"meta_title": "...",
"meta_description": "...",
"excerpt": "..."
}
I then parse that JSON with a JSON > Parse JSON module.
Could I compress this into one call? Sure. I tried that. The meta fields got worse and harder to constrain. So I split it.
Module 6: Write everything back to Google Sheets
At this point I have:
- Cleaned title and slug.
- Generated HTML body.
- Meta title, description, and excerpt.
Before touching Ghost I push this back into the sheet.
I use Google Sheets > Update a Row with:
-
Column J →
generated_htmlfrom OpenAI. -
Column K →
meta_titlefrom JSON. -
Column L →
meta_description. -
Column M →
excerpt. - Column D → final slug.
-
Column B → set to
generated.
Why write back before publishing?
Because I like a manual checkpoint. I can read the generated HTML in Sheets, tweak the brief or title, flip status back to ready if I want a regen.
This effectively makes Sheets my content staging area while Ghost stays clean.
Module 7: Filter on publish_flag
Not every generated post should go live.
I add a filter between the Sheets update and the Ghost modules:
publish_flag = "draft" OR publish_flag = "publish"
If publish_flag is empty or something else, the scenario stops for that row. It will still update the generated fields, but nothing touches Ghost.
When I want a post to move to Ghost I set publish_flag to draft or publish in the sheet.
Module 8: Router for create vs update in Ghost
I use Ghost both for new posts and edits triggered from Sheets.
So I drop a Router with two branches:
-
Branch 1: Create when
ghost_post_idis empty. -
Branch 2: Update when
ghost_post_idis not empty.
This lets me tweak a title or tags in Sheets, set publish_flag again, and push an update to the existing Ghost post.
Module 9: Ghost “Create a post” (branch 1)
On the create branch I use the Ghost > Create a post module.
Here is how I map things:
-
Title → Sheet
title. - Slug → final slug from Module 3.
-
HTML →
generated_html. - Excerpt → AI generated excerpt.
- Meta title → meta_title.
- Meta description → meta_description.
- Tags → split tags column on comma, trim each value.
For status I map directly from publish_flag:
-
draft→ Ghost draft. -
publish→ Ghost published.
Ghost returns a post object with an id. I capture that and write it back to the sheet.
Module 10: Google Sheets “Update a Row” with ghost_post_id
Immediately after the create call I drop another Google Sheets > Update a Row module.
It takes the id from Ghost and stores it in column O: ghost_post_id. I also set:
-
status = publishedif publish_flag waspublish. -
status = generatedif publish_flag wasdraft.
This keeps my spreadsheet the single source of truth. I can filter ideas, generated drafts, and published content without logging into Ghost.
Module 11: Ghost “Update a post” (branch 2)
On the update branch, I use Ghost > Update a post with the stored ghost_post_id.
I do not blindly overwrite everything. Here is my mapping logic:
- Always update HTML from
generated_html. - Update title and slug only if the sheet values changed.
- Update meta fields if they exist in Sheets. Otherwise keep Ghost values.
- Tags are rebuilt from the
tagscolumn every time.
In Make that means I use if expressions in the field mapping. For example:
if(
empty(sheet_meta_title);
ghost.meta_title;
sheet_meta_title
)
I apply a similar pattern for slug and description.
Module 12: Error handling and logging
I do not like silent failures.
So I added a simple error path using Make’s built-in error handlers:
- If Ghost or OpenAI throws an error, I catch it in a separate route.
- Then I write the error message into a
errorscolumn in Sheets. - I also ping myself with a Gmail > Send an email module when something breaks.
Nothing fancy. Enough to see when the pipeline has issues without watching Make all day.
Why I structured it this way
I tried a few alternative versions before landing on this flow.
What I did not like:
- Single gigantic OpenAI prompt that tried to output HTML, meta, title variants, tag suggestions, everything. It was brittle.
- Direct Sheets to Ghost without writing generated content back first. Harder to see what the model produced.
- No
publish_flag, so everything went straight to draft in Ghost. That got noisy fast.
What works better for me now:
- Sheets is the control tower. All fields visible. Easy to override.
- Make is the transport layer. No business logic hidden in Ghost.
- OpenAI is a dumb transformer that takes structured inputs and outputs structured HTML and metadata.
The end result is that I spend time on outlines and ideas, not formatting posts or fighting CMS UI.
How this feels in daily use
Here is my actual workflow on a random Tuesday.
- I open the
Postssheet and dump three new ideas as rows with rough titles and briefs. - I set
status = readyand leavepublish_flagempty. - Make picks them up, runs OpenAI, writes back generated HTML and meta, sets
status = generated. - I skim the HTML in Sheets. If a post feels off, I tweak the brief or title, flip status back to
readyand let it regenerate. - Once I like a post, I set
publish_flag = draftorpublish. - Next run, Make pushes it into Ghost accordingly.
I still edit inside Ghost sometimes. Especially for code-heavy posts or things where I want to insert images manually.
But 80 percent of the grunt work is automated. Which is the point.
If you want to copy this
If you are already using Make, you can rebuild this in an afternoon.
The three non-negotiables in my experience:
- Use Sheets as a real schema, not a random dumping ground.
- Keep OpenAI prompts tight and specific. One job per call.
- Always round trip data back into Sheets so you can see what the system did.
From there you can layer on extras. Social post generation, image generation, cross-posting to other platforms. I keep those in separate scenarios so this core pipeline stays boring and reliable.
That is the full blueprint I run right now. It will probably change again in a month. But the core idea stays the same. Treat your content as structured data, not as a CMS typing session.
Solucionar Timeouts de MCP: Patrón HandleId Asíncrono
Las herramientas MCP congelan a los agentes de IA cuando las APIs externas son lentas, causando errores 424. El patrón handleId asíncrono devuelve inmediatamente un ID de trabajo y consulta los resultados sin bloquear.
El timeout de herramienta MCP ocurre cuando un agente de IA llama a una herramienta del Protocolo de Contexto de Modelo (MCP) que depende de una API externa lenta. La herramienta bloquea al agente indefinidamente en lugar de devolver un error. El resultado es un error 424 (Failed Dependency) o un flujo de trabajo congelado sin retroalimentación al usuario. Este post muestra el problema con escenarios reales y cómo el patrón handleId asíncrono proporciona respuestas inmediatas.
Esta demo usa Strands Agents con MCP (Model Context Protocol). El patrón asíncrono es independiente del framework y aplica a cualquier agente que llame APIs externas a través de MCP.
Código funcional: github.com/aws-samples/sample-why-agents-fail
Serie: Por Qué Fallan los Agentes de IA
- Desbordamiento de Ventana de Contexto — Patrón de Puntero de Memoria para datos grandes
- Herramientas MCP Que Nunca Responden (este post) — Patrón asíncrono para APIs externas lentas
- Loops de Razonamiento en Agentes de IA — Detectar y bloquear llamadas repetidas a herramientas
El Problema: Herramientas MCP Que Nunca Responden
El Protocolo de Contexto de Modelo (MCP) permite a los agentes de IA llamar herramientas externas. Pero cuando esas herramientas dependen de APIs lentas, todo el flujo de trabajo del agente se congela. El agente espera. El usuario espera. No pasa nada.
Una observación comunitaria de Octopus (Resilient AI Agents With MCP, 2025) identifica el problema central: a medida que aumentan las integraciones de sistemas externos, también aumenta la probabilidad de fallo. Los sistemas dejan de estar disponibles, responden lentamente o devuelven errores. Los agentes no tienen una estrategia incorporada para manejar esto.
Los reportes de OpenAI Community confirman el impacto del mundo real:
- Errores 424 cuando las herramientas MCP tardan demasiado
- Estados sin respuesta donde las solicitudes ni tienen éxito ni fallan
- Herramientas que pasan la validación de handshake pero hacen timeout durante la ejecución
Por Qué Sucede Esto
MCP espera que las herramientas respondan rápidamente. Cuando una herramienta llama a una API externa lenta.
El protocolo MCP tiene expectativas de timeout implícitas. Si la herramienta no responde dentro de ~7-10 segundos, la conexión puede caerse con un error 424 (Failed Dependency). El agente recibe un error en lugar de datos, y el usuario no obtiene una respuesta útil.
Tres modos de fallo:
- API lenta — La herramienta espera 15+ segundos, UX pobre pero eventualmente responde
- API fallida — Servicio externo no disponible, error 424 después del timeout
- Estado sin respuesta — Solicitud aceptada pero nunca devuelve, requiere reinicio de sesión
La Demo: Simulando Escenarios Reales de Timeout
Construimos un servidor MCP que simula estos escenarios del mundo real:
from mcp.server import FastMCP
import asyncio
# FastMCP es un framework ligero de servidor MCP — las herramientas se registran con @mcp.tool()
mcp = FastMCP("Timeout Demo Server")
# Línea base: responde en 1s, bien dentro del umbral de timeout implícito de MCP (~7-10s)
@mcp.tool(description="Fast API - responds in 1 second")
async def fast_api(query: str) -> str:
await asyncio.sleep(1)
return f"Fast result for: {query}"
# Caso problema: retraso de 15s excede timeout de MCP — el agente se congela esperando esto
@mcp.tool(description="Slow API - responds in 15 seconds")
async def slow_api(query: str) -> str:
await asyncio.sleep(15) # Simula un servicio externo lento (pipeline de datos, trabajo por lotes)
return f"Slow result for: {query}"
# Caso de fallo: retraso de 7s activa el timeout, luego lanza Failed Dependency (424)
@mcp.tool(description="Failing API - returns 424 after delay")
async def failing_api(query: str) -> str:
await asyncio.sleep(7)
raise Exception("Failed Dependency: External service unavailable")
La Solución Async HandleId
En lugar de esperar operaciones lentas, devuelve inmediatamente con un ID de seguimiento:
import uuid
# Almacén de trabajos en memoria: mapea job_id → {status, query, result}
# Para producción, reemplazar con un almacén persistente (Redis, DynamoDB) para durabilidad entre reinicios
JOBS = {}
# El patrón handleId: devuelve un ID de seguimiento inmediatamente en lugar de bloquear
@mcp.tool(description="Start a long-running job, returns immediately with job ID")
async def start_async_job(query: str) -> str:
job_id = str(uuid.uuid4())[:8] # ID corto que el LLM puede pasar en llamadas de seguimiento
JOBS[job_id] = {"status": "processing", "query": query}
# Fire-and-forget: el trabajo lento se ejecuta en segundo plano, la herramienta devuelve antes de que termine
asyncio.create_task(do_work(job_id, query))
# El agente recibe esto en < 1s — sin timeout, sin UI congelada
return f"Job started: {job_id}. Use check_job_status to poll for results."
# Endpoint de consulta: el agente llama a esto repetidamente hasta que el estado es "completed"
@mcp.tool(description="Check status of a running job")
async def check_job_status(job_id: str) -> str:
job = JOBS.get(job_id)
if not job:
return f"Job {job_id} not found"
if job["status"] == "completed":
return f"COMPLETED: {job['result']}" # Devuelve el resultado real al agente
return f"PROCESSING: Job {job_id} still running" # El agente consulta de nuevo después de una breve espera
Resultados de la Demo
Probamos los cuatro escenarios con un Strands Agent conectado al servidor MCP:
| Escenario | Tiempo de Respuesta | Experiencia de Usuario | Hallazgo de Investigación |
|---|---|---|---|
| Fast API (retraso 1s) | 3.2s total | ✅ Buen UX | Línea base |
| Slow API (retraso 15s) | 17.8s total | ❌ UX pobre — agente espera | Octopus: “el agente espera indefinidamente” |
| Failing API (424) | 7.7s total | ❌ Error después de esperar | OpenAI Community: errores 424 |
| Patrón asíncrono (handleId) | 3.7s total | ✅ Respuesta inmediata | Solución: “responder ASAP con handleId” |
El patrón asíncrono transforma una espera de 17.8s en una respuesta inmediata de 3.7s. El agente le dice al usuario “trabajo iniciado” y puede verificar el estado más tarde, sin UI congelada y sin errores de timeout.
¿Por Qué Strands Agents para Integración MCP?
El MCPClient se conecta a cualquier servidor MCP en dos líneas. El agente descubre herramientas disponibles en tiempo de ejecución a través de list_tools_sync(), así que no mantienes una lista de herramientas codificada. Cuando el servidor MCP implementa el patrón handleId asíncrono, el agente consulta automáticamente sin código de orquestación adicional.
Strands soporta múltiples proveedores de modelos (OpenAI, Amazon Bedrock, Anthropic, Ollama). Los patrones de timeout de MCP mostrados aquí funcionan idénticamente en todos los proveedores.
Cuándo Usar Cada Patrón
Llamada directa (herramientas rápidas < 5s):
- Búsquedas, cálculos, llamadas pequeñas a API
- Sin riesgo de timeout
HandleId asíncrono (herramientas lentas > 5s):
- Llamadas a API externas con latencia impredecible
- Procesamiento de datos, generación de reportes
- Cualquier operación que pueda exceder el timeout de MCP
Reintento con backoff (fallos intermitentes):
- Servicios que ocasionalmente fallan pero se recuperan
- Operaciones dependientes de red
Pruébalo Tú Mismo
Necesitas Python 3.9+, uv, y una clave API de OpenAI. El servidor MCP se ejecuta localmente como un subproceso, así que no se necesitan servicios externos.
git clone https://github.com/aws-samples/sample-why-agents-fail
cd sample-why-agents-fail/stop-ai-agents-wasting-tokens/02-mcp-timeout-demo
uv venv && uv pip install -r requirements.txt
export OPENAI_API_KEY="tu-clave-aquí"
uv run python test_mcp_timeout.py # Ejecuta los 4 escenarios
O abre test_mcp_timeout.ipynb en Jupyter, JupyterLab, VS Code, o tu entorno de notebook preferido.
Conclusiones Clave
- Las herramientas MCP hacen timeout silenciosamente — errores 424 sin recuperación
- Las APIs lentas congelan todo el agente — espera de 17.8s sin retroalimentación
- El patrón handleId asíncrono lo soluciona — respuesta inmediata, consultar por resultados
- Diseña para el fallo — cada llamada externa puede hacer timeout, planifica en consecuencia
Preguntas Frecuentes
¿Qué causa errores 424 en llamadas a herramientas MCP?
Un error 424 (Failed Dependency) ocurre cuando una herramienta MCP tarda más que el umbral de timeout implícito (típicamente 7-10 segundos) en responder. El protocolo MCP espera que las herramientas devuelvan resultados rápidamente. Cuando una API externa bloquea la herramienta más allá de este umbral, la conexión se cae y el agente recibe un error 424 en lugar de datos.
¿Cuándo debo usar el patrón handleId asíncrono en lugar de una llamada directa a herramienta MCP?
Usa el patrón handleId asíncrono para cualquier herramienta que llame a una API externa con latencia impredecible: procesamiento de datos, generación de reportes, llamadas a servicios de terceros, o cualquier operación que pueda exceder 5 segundos. Para búsquedas rápidas, cálculos y llamadas pequeñas a API por debajo de 5 segundos, las llamadas directas funcionan bien.
¿El patrón handleId asíncrono funciona con cualquier servidor MCP, no solo Strands?
Sí. El patrón handleId asíncrono es un patrón de diseño de servidor MCP, no una característica de framework. Cualquier agente compatible con MCP puede llamar herramientas start_long_job y check_job_status. El patrón funciona con OpenAI Agents, integraciones MCP de LangChain, y cualquier cliente que soporte el Protocolo de Contexto de Modelo.
Referencias
Investigación
- Resilient AI Agents With MCP: Timeout And Retry Strategies — Octopus blog (observación comunitaria), May 2025
- Call remote MCP server tool timed out, error 424 — OpenAI Community (foro comunitario)
- Handling Timeouts with Long-Running MCP Connectors — OpenAI Community (foro comunitario), Dec 2025
- Build Timeout-Proof MCP Tools — Arsturn (observación comunitaria)
Implementación
- Strands MCP Tools — Connect any MCP server
- Strands Model Providers — Swap to Amazon Bedrock, Anthropic, Ollama
Gracias!
I Built a ‘Production-Ready’ AI Agent Framework. It Was a Lie. So I Fixed It.
I Built a “Production-Ready” AI Agent Framework. It Was a Lie. So I Fixed It.
A story about AI-generated repos, fake READMEs, and what I did about it.
Two weeks ago, I built an automation pipeline that generates open-source projects on GitHub. The idea: market-research trending tech, scaffold a repo, fill it with code, add docs, and push — fully automated. 14 agents working together. 65 seconds end-to-end. I was proud of it.
Then I actually looked at the generated code.
The Ugly Truth
The pipeline had generated three repos:
- ai-agent-starter: “Production-ready AI agent framework with multi-provider support”
- dev-cli-kit: “Modern CLI toolkit with scaffolding, code generation, and git automation”
- nextjs-saas-starter: “Next.js SaaS starter with auth, payments, and AI integration”
Sounds great, right? Here’s what was actually inside:
| Repo | Source Code | Tests | Could It Run? |
|---|---|---|---|
| ai-agent-starter | 183 lines of broken code | 0 | No |
| dev-cli-kit | 1 Handlebars template file (not even .ts) |
0 | No |
| nextjs-saas-starter | Zero lines of code | 0 | No |
The nextjs-saas-starter didn’t even have a src/ directory. Just a package.json, a tsconfig.json, and a README claiming it had “authentication, Stripe payments, and an AI chat interface.” None of those existed. Not even a single component.
The READMEs were AI-generated marketing fluff. Every claim was false. And somehow, each one had donation links (PayPal, Buy Me a Coffee, GitHub Sponsors) — asking for money before delivering a single working line of code.
I had built a lying machine.
What I Did
Instead of deleting the repos (GitHub URLs and star history have value), I decided to make every claim true.
I rewrote all three from scratch, and built a fourth:
1. ai-agent-starter (v0.2.0)
What it claimed → What it now is:
- “Multi-provider support (OpenAI, Anthropic, Ollama)” → Actually implemented all three, with streaming
- “Built-in tool execution engine” → Real function calling via OpenAI/Anthropic native tool APIs, not regex string matching
- “Conversation memory” → Token-aware memory manager with auto-pruning
- “CLI and programmatic API” →
npx ai-agent-starter chat,npx ai-agent-starter run "prompt", and importable package
650+ lines of TypeScript, 24 tests, zero build errors.
2. dev-cli-kit (v0.2.0)
The old version’s only source file was cli.ts.hbs — a Handlebars template with // TODO: Implement init logic as its core functionality.
Now: real project scaffolding from 3 built-in templates (Node/TS, React/Vite, CLI tool), code generation (component/hook/util/api), and git workflow helpers. 480+ lines, 8 tests.
3. nextjs-saas-starter (v0.2.0)
From zero lines to a complete Next.js 14 app: landing page (hero, features grid, pricing), dashboard with sidebar + stats, login/register pages, billing page, settings page, API route, Tailwind CSS with dark mode. 520+ lines across 14 files.
4. ai-chat-saas (NEW)
Built on top of the first three — a complete AI chat application. Multi-provider streaming chat with server-side API key protection. 763 lines. Ready to deploy on Vercel in 60 seconds.
What I Learned
-
AI-generated code isn’t the problem — unverified AI-generated code is. My pipeline agents were generating template stubs and calling them “production-ready.” The fix was embedding real, tested code as the generation template.
-
READMEs should describe what EXISTS, not what you WISH existed. Every line in the new READMEs corresponds to actual code you can run.
-
Donation links on empty repos are a red flag. If a project has zero working code and asks for money, it damages trust in the entire open-source ecosystem. I removed all donation links and added them back only where appropriate.
-
“Production-ready” means something. It means tests pass, builds succeed, and someone can clone your repo, run
npm install && npm run dev, and see a working product. Not a TODO comment.
What’s Next
The pipeline is fixed. The code agents now generate real, working code instead of stubs. Cron runs daily at 16:00 Beijing time.
I’m also launching a paid template — ai-chat-saas — on Gumroad for $19. It’s the chat application with all three providers, streaming, dark mode, and server-side API protection. Clone, set your API keys, deploy.
The moral: if you’re using AI to generate code, verify it. Actually run npm install. Actually run npm test. Actually try to use it. Otherwise you’re just building a very fast bullshit machine.
All repos: github.com/GrahamduesCN


