storefront-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@storefront-mcpbusca departamentos en Coyoacán para 2 personas"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
storefront-mcp
Servidor MCP con MCP Apps (SEP-1865, spec 2026-01-26) para el catálogo de alquiler temporario de "Vidriera" (storefront-demo/). Cuando el modelo busca propiedades, la respuesta trae un recurso de UI — un iframe con tarjetas, navegación a detalle y un flujo de reserva simulado de punta a punta — que el host (Claude Desktop) renderiza dentro del chat.
Es una PoC local: el catálogo es una copia congelada de storefront-demo/data/catalog.json, no hay base de datos ni red hacia el front.
El servidor tiene dos entrypoints sobre la misma factory (createStorefrontMcpServer() en src/server.ts), con las mismas tools y el mismo recurso ui://storefront-mcp/catalog.html en ambos casos:
src/main.ts→ stdio, para Claude Desktop local.src/http-main.ts→ Streamable HTTP stateless, para desplegar como Web Service (Render u otro).
Instalación
npm install
npm run build # compila el iframe (vite) + el servidor (tsc), ambos entrypointsnpm run build:view recompila sólo src/view/ a dist-view/mcp-app.html (un único archivo, JS y CSS inlineados vía vite-plugin-singlefile); npm run build:server recompila sólo el servidor (ambos entrypoints, dist/main.js y dist/http-main.js).
Related MCP server: Rentalot MCP Server
Modo local con Claude Desktop (stdio)
npm install
npm run build
npm startnpm start (= npm run start:stdio) corre node dist/main.js por stdio (para que lo levante Claude Desktop u otro host). Además levanta un servidor HTTP local aparte, sólo para servir la página estática de "pago simulado" (ver Gaps), en el puerto 4405 por defecto (STOREFRONT_MCP_PAY_PORT para cambiarlo).
Servidor HTTP local
PUBLIC_BASE_URL=http://localhost:3000 npm run start:httpnpm run start:http corre node dist/http-main.js, un servidor Streamable HTTP stateless (sin sesiones, sin SSE legacy) escuchando en 0.0.0.0:${PORT} (PORT default 3000). Expone:
http://localhost:3000/mcp (POST/GET/DELETE — Streamable HTTP)
http://localhost:3000/health (GET — health check)
http://localhost:3000/mock-pay (GET — página de pago simulado)PUBLIC_BASE_URL es la URL pública desde la que se sirve este mismo servidor; se usa para armar el link de "Ir a pagar" que genera start_mock_booking (${PUBLIC_BASE_URL}/mock-pay?...). Si no la definís, cae en http://localhost:${PORT} — sirve para probar en local sin configurar nada.
Config para Claude Desktop
Agregá esto a claude_desktop_config.json, con la ruta absoluta a este repo:
{
"mcpServers": {
"storefront-mcp": {
"command": "node",
"args": ["/ruta/absoluta/a/storefront-mcp/dist/main.js"]
}
}
}Ubicación del archivo de config según el SO:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonLinux: Claude Desktop no tiene build oficial; si usás un paquete no oficial (p. ej. el de AUR), la ruta suele ser
~/.config/Claude/claude_desktop_config.json— verificá con esa distribución en particular, y tené en cuenta que el soporte de MCP Apps ahí no está garantizado igual que en el build oficial.
Después de editar el config, reiniciá Claude Desktop. Acordate de correr npm run build antes (Claude Desktop ejecuta dist/main.js ya compilado, no compila nada por su cuenta).
Despliegue en Render
Configuración del Web Service:
Runtime: Node
Build Command: npm ci && npm run build
Start Command: npm run start:http
Health Check Path: /healthVariable de entorno:
PUBLIC_BASE_URL=https://<nombre-del-servicio>.onrender.comNo hace falta configurar PORT a mano — Render la inyecta y src/http-main.ts la lee de process.env.PORT (default 3000 sólo si corrés el servidor fuera de Render sin definirla). El servidor escucha en 0.0.0.0, como pide Render, y no abre ningún otro puerto público (el server aparte del puerto 4405 es exclusivo del modo stdio y no se levanta en http-main.ts).
Una vez desplegado, para registrar el server remoto en un host compatible con MCP Apps sobre Streamable HTTP, la URL del endpoint es https://<nombre-del-servicio>.onrender.com/mcp.
Cómo ver el iframe funcionando
En una conversación nueva de Claude Desktop con el servidor conectado, pedí algo como:
Buscame departamentos en Coyoacán para 2 personas
El modelo debería llamar a search_listings y el chat debería mostrar el iframe con 3 tarjetas. Desde ahí: click en una tarjeta → detalle completo (descripción, amenities, anfitrión) → "Reservar" → resumen con fechas de ejemplo y total → "Ir a pagar" abre en el navegador la página estática de pago simulado (no cobra nada, no procesa nada).
Si el host no soporta MCP Apps, la tool sigue devolviendo una respuesta de texto legible (ver Gaps).
Arquitectura
src/
main.ts # entrypoint stdio: StdioServerTransport + arranca mock-pay (puerto aparte)
http-main.ts # entrypoint HTTP: Streamable HTTP stateless + /health + /mock-pay
server.ts # createStorefrontMcpServer() — misma factory para los dos entrypoints
mock-pay.ts # servidor HTTP aparte (puerto 4405), sólo lo usa main.ts (stdio)
mock-pay-page.ts # renderPayPage(): el HTML de "pago simulado", sin duplicar
catalog-data.ts # carga data/catalog.json (copia del front)
catalog-repository.ts # búsqueda/detalle sobre esa copia
format.ts # formateo de precio/huéspedes (portado del front)
types.ts # tipos de dominio
view/
mcp-app.html # el recurso de UI: HTML + CSS (Barragán, replica el front)
src/mcp-app.ts # lógica: App SDK, navegación, tool calls, openLink
data/
catalog.json # copia de storefront-demo/data/catalog.json
scripts/
smoke-stdio.mjs # smoke test JSON-RPC real sobre stdio (dist/main.js)
smoke-http.mjs # smoke test Streamable HTTP real (dist/http-main.js)
smoke-lib.mjs # aserciones compartidas entre los dos smoke testsLos dos entrypoints, un solo server
createStorefrontMcpServer({ payBaseUrl }) (en src/server.ts) registra exactamente las mismas tools y el mismo recurso en los dos modos — no hay lógica de negocio duplicada entre stdio y HTTP:
main.tsllama a la factory una vez y conecta esa única instancia a unStdioServerTransport(el proceso vive mientras dure la conexión con el host).http-main.tssigue el patrón oficial stateless del SDK instalado (@modelcontextprotocol/sdk@1.30.0, ejemplosimpleStatelessStreamableHttp.ts): crea una instancia nueva deMcpServer+StreamableHTTPServerTransport(consessionIdGenerator: undefined) en cadaPOST /mcp, las conecta, atiende esa única request y las cierra al terminar. Ninguna instancia se reconecta ni se reutiliza entre requests — así evita el error de "transport ya conectado a otro server" que da el SDK si se intenta conectar dos veces la misma instancia.
Sólo payBaseUrl difiere entre modos: main.ts resuelve PUBLIC_BASE_URL (o cae a http://localhost:${STOREFRONT_MCP_PAY_PORT ?? 4405}); http-main.ts resuelve PUBLIC_BASE_URL (o cae a http://localhost:${PORT}, el propio servidor).
Tools
Tool | Visibilidad | Qué hace |
| modelo + app | Busca por colonia/huéspedes/precio máx/tipo/texto libre. Devuelve hasta 3 tarjetas + el recurso de UI. |
| modelo + app | Ficha completa de una propiedad por ID. La usa el modelo (texto) y el iframe ya abierto, vía |
| sólo app | Genera fechas de ejemplo, total y el link de pago simulado. Oculta al modelo a propósito — no tiene sentido que el LLM decida iniciar un "cobro" por su cuenta. |
Qué se copió del front (y desde dónde)
El front (storefront-demo/) es la fuente de la verdad. Todo lo de acá abajo es una copia manual, congelada a hoy (2026-07-28). Si el front cambia, hay que repetir la copia a mano — no hay ningún proceso automático.
Datos:
data/catalog.json← copia byte a byte destorefront-demo/data/catalog.json(220 listings, mismo esquema, sin renombrar campos). Versrc/catalog-data.tsysrc/types.ts.Paleta y estilos:
src/view/mcp-app.html(bloque<style>) ← replica a mano las variables y clases destorefront-demo/app/globals.css(paleta Barragán: rosa/añil/piedra,shadow-hard,.skeleton).Layout de tarjeta: replica
storefront-demo/components/ListingCard.tsx+StarRating.tsx.Fallback de foto rota: replica
storefront-demo/components/PhotoWithFallback.tsx(mismo array de colores de "muro" por seed, mismo ícono de ventana en SVG).Layout de detalle: replica
storefront-demo/app/listing/[id]/page.tsx(precio + estadía mínima, descripción, grilla de amenities, anfitrión).
No hay cambios pendientes ni necesarios en storefront-demo/ — este servidor sólo lee su copia local, nunca el repo del front.
Tests
npm run build # necesario antes: los smoke tests corren contra dist/
npm test # smoke-stdio.mjs + smoke-http.mjsNo hay tests unitarios ni un framework de testing — son dos smoke tests que hablan el protocolo real:
scripts/smoke-stdio.mjs: levantadist/main.js, habla JSON-RPC por stdio (initialize,tools/list,tools/call×3,resources/read) y valida que las tools,_meta.ui.resourceUriy el recursoui://storefront-mcp/catalog.htmlrespondan con las formas esperadas.scripts/smoke-http.mjs: levantadist/http-main.jsen un puerto de prueba conPUBLIC_BASE_URLapuntando a sí mismo, y repite las mismas validaciones (initialize,tools/list,tools/call,resources/read) pero víafetchreal contraPOST /mcp, más/health,GET/DELETE /mcp(deben dar 405 con error JSON-RPC, como el ejemplo oficial stateless) yGET /mock-pay. También confirma quepayUrlusaPUBLIC_BASE_URLy no tiene doble barra.
Ninguno de los dos reemplaza al otro — corren la misma lógica de negocio por los dos transportes.
Cómo verifiqué esto (sin Claude Desktop a mano)
No tenía un host MCP Apps real en este entorno para probar el iframe de punta a punta, así que además de los smoke tests verifiqué el lado del iframe por separado: un harness HTML standalone hace de "host" falso (responde ui/initialize con hostContext/hostCapabilities, reenvía tools/call a datos de prueba, responde ui/open-link) sirviendo el dist-view/mcp-app.html real dentro de un iframe, manejado con Playwright. Confirmé visualmente el recorrido completo: lista → detalle → reserva → "Ir a pagar" (con la URL correcta) → volver → placeholder de foto rota con una URL que de verdad no resuelve → tema oscuro vía hostContext.theme.
Igual, la prueba definitiva del iframe es correrlo en Claude Desktop (local) o en el host remoto que uses (HTTP) — el harness no reemplaza eso, sólo reduce el riesgo de haber usado mal la spec de MCP Apps. El transporte Streamable HTTP en sí (/mcp, /health, /mock-pay) sí está probado de punta a punta con tráfico HTTP real vía smoke-http.mjs.
Gaps y decisiones de implementación
Link de pago mock no es un
data:URI: la spec permite queui/open-linkrechace URLs como inválidas ("Invalid URL" / "Policy violation"), ydata:URIs son un candidato típico a bloqueo por política de host. En vez de arriesgarme, en modo stdio agregué un servidor HTTP local aparte (src/mock-pay.ts, puerto4405) que sirve una página estática real; en modo HTTP, esa misma página es simplemente otra ruta (GET /mock-pay) del único servidor público.Sin CORS: no agregué
cors()ni ningún header de CORS. El ejemplo oficialsimpleStatelessStreamableHttp.tsde la versión instalada tampoco lo usa, y los clientes remotos de MCP (incluido un host llamando a este servidor) no son fetch de browser cross-origin típico — agregarlo sin necesidad hubiera sido exactamente el "CORS permisivo" que pedía evitar. Lo que sí agregué es la protección de DNS rebinding que ya traecreateMcpExpressApp(): siPUBLIC_BASE_URLestá definida, se pasa su hostname comoallowedHosts(rechaza requests conHostheader distinto); si no está definida (dev local pegándole a0.0.0.0sin configurar nada), queda sin esa restricción, igual que el comportamiento por defecto del SDK.enableJsonResponse: true: el transporte Streamable HTTP por defecto responde con un stream SSE de un solo evento. Para esta PoC (tools de request/respuesta simple, sin notificaciones de progreso) usé el modo de respuesta JSON plano que ya ofrece el SDK — sigue siendo Streamable HTTP válido (es el modo "JSON response, no SSE" documentado), sólo que más simple de consumir para un cliente API-style.Sin fuente Archivo (la del front): cargarla implicaba declarar
resourceDomainsparafonts.googleapis.com/fonts.gstatic.comen el CSP del recurso y sumar una dependencia de red externa a una PoC que se pensó "local". Uso una pila de fuentes del sistema (ui-sans-serif, system-ui, ...) con la misma jerarquía tipográfica. Si esto importa para la demo, es un cambio de una línea en_meta.ui.csp.resourceDomains+ un@importen el CSS.resourceDomainspara fotos: el CSP por defecto de MCP Apps esimg-src 'self' data:— bloquearía las fotos (picsum.photos) por completo. Se declaróhttps://picsum.photosexplícitamente en_meta.ui.csp.resourceDomainsdel recurso.Sin fallback de texto por falta de soporte del host: la spec dice que los servidores deberían (
SHOULD, noMUST) chequeargetUiCapability()en elinitializedel cliente y registrar una variante de tool sin_meta.uisi el host no soporta MCP Apps. No lo implementé — esta PoC apunta específicamente a Claude Desktop, que lo soporta nativamente desde el 26 de enero de 2026, y sumar esa rama duplicaba la lógica de las tools sin un caso de uso real acá. Si en algún momento hace falta targetear un host sin soporte, el punto de entrada esgetUiCapabilityde@modelcontextprotocol/ext-apps/server, documentado en la spec.Sin fechas/disponibilidad reales:
catalog.jsoncopiado tienemeta.hasAvailability: falsey ningún listing traeavailability— por esosearch_listingsno filtra por check-in/checkout (a diferencia delcatalog-repository.tsdel front, que sí tiene esa lógica lista para cuando haya datos). Las fechas de la reserva simulada son sintéticas (próximo viernes + N noches), como pide la consigna ("fechas de ejemplo").Cap de resultados:
search_listingssiempre devuelve como máximo 3 propiedades (no configurable por el modelo más allá de bajarlo), acorde a "unas tres opciones".
Available Tools
3 toolsget_listing_detailVer detalle de una propiedadA
Devuelve la ficha completa de una propiedad del catálogo (descripción, amenities, anfitrión) dado su ID numérico. Usala para responder preguntas puntuales sobre una propiedad que ya apareció en una búsqueda previa.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | ID de la propiedad (el mismo que devuelve search_listings). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Without annotations, the description carries the full burden. It clearly states it's a read operation returning property details. No contradictions. It could mention error handling (e.g., if ID not found) or authentication, but for a simple read tool, this is sufficient.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence in Spanish, no wasted words. Front-loaded with the main action and purpose. Perfectly concise for the complexity of the tool.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no output schema, the description outlines what the result includes (description, amenities, host). For a one-parameter read tool, this is adequate. Could detail response language or missing ID behavior, but the tool's simplicity limits the gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema has 100% coverage for the single parameter 'id'. The description adds context: 'dado su ID numérico' and the schema description ties it to 'search_listings'. This adds meaning beyond raw type/description, linking the parameter to its source.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('Devuelve') and resource ('ficha completa de una propiedad del catálogo'), listing included fields ('descripción, amenities, anfitrión') and condition ('dado su ID numérico'). This clearly distinguishes it from sibling tools like 'search_listings' which returns lists.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit usage context: 'Usala para responder preguntas puntuales sobre una propiedad que ya apareció en una búsqueda previa'. This tells the agent when to use this tool (after a search) and implies not to use for initial searches. It also references the sibling 'search_listings' for the ID source.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_listingsBuscar propiedadesA
Busca alojamientos en el catálogo de alquiler temporario en CDMX (Vidriera) por colonia, cantidad de huéspedes, precio máximo por noche, tipo de habitación o texto libre. Devuelve hasta 3 opciones como tarjetas visuales interactivas para que el usuario elija, navegue el detalle y arranque una reserva. Usala cuando el usuario pida alojamiento, quiera comparar opciones o pregunte qué hay disponible en una zona de la ciudad.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Texto libre para buscar en nombre/descripción. | |
| guests | No | Cantidad mínima de huéspedes. | |
| priceMax | No | Precio máximo por noche, en MXN. | |
| roomType | No | Tipo de habitación tal cual viene en el catálogo, ej. 'Entire home/apt' o 'Private room'. | |
| neighbourhood | No | Colonia o alcaldía de CDMX, ej. 'Coyoacán' o 'Roma Norte'. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full burden. It reveals that the tool returns up to 3 interactive visual cards for selection and navigation to detail/booking. It does not cover error handling or authentication, but for a search tool this is adequate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single well-structured paragraph. Every sentence adds value: first states function, then output behavior, then usage guidance. No unnecessary words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 5 parameters, no output schema, and no annotations, the description covers the output as visual cards and usage context. It lacks details like sorting, default behavior, or error info, but is reasonably complete for a search tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, with clear parameter descriptions. The description adds context about the catalog and city but does not significantly enhance the schema's information. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool searches for temporary rentals in CDMX using various filters like neighborhood, guests, price, room type, and free text. It specifies the resource and action, and differentiates from siblings 'get_listing_detail' and 'start_mock_booking'.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly tells when to use the tool: when the user asks for accommodation, wants to compare options, or asks what's available. It implies alternatives via sibling names but does not explicitly state when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
start_mock_bookingIniciar reserva simuladaB
Genera un resumen de reserva mock (fechas de ejemplo, total) y el link de pago simulado.
| Name | Required | Description | Default |
|---|---|---|---|
| guests | No | ||
| nights | No | ||
| listingId | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so description carries full burden. It indicates the output is mock and simulated, implying no real side effects, but does not explicitly state safety or data mutation details.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single sentence that is concise. However, the mix of Spanish title and English description might cause confusion.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Lacks details on parameter usage and output format. With no output schema, the description should clarify what the summary and link contain, but it does not.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and the description does not mention any of the three parameters (listingId, guests, nights). It provides no meaning beyond the schema's field names and types.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool generates a mock booking summary and simulated payment link, distinguishing it from sibling tools like search_listings and get_listing_detail which deal with listing information.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No explicit guidance on when to use this tool versus alternatives. The purpose is implied for testing, but no 'when to use' or 'when not to use' is provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
get_listing_detail - First observed
search_listings - First observed
start_mock_booking
TDQS
Scored across 3 tools
Each tool has a distinct function: search for listings, get detail of a specific listing, and start a booking. No overlap in purpose.
All tools follow a consistent verb_noun pattern with snake_case: search_listings, get_listing_detail, start_mock_booking.
Three tools is minimal but covers the core workflow of searching, viewing details, and booking. Slightly under for a full-featured storefront, but acceptable.
Covers search, detail, and booking, but lacks common operations like canceling or modifying bookings, or managing user profiles. Notable gaps exist.
Maintenance
Related MCP Connectors
Hotel booking MCP server. Search, book, and manage reservations across 250K+ properties worldwide.
Flight search MCP server providing search, pagination, and itinerary details for AI assistants.
MCP server for innovationlab documentation, generated by doc2mcp.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables users to search Airbnb listings with advanced filtering options and retrieve detailed property information through an MCP server interface.21,584 npmMIT

Rentalot MCP Serverofficial
AlicenseNot gradedqualityCmaintenanceMCP server for the Rentalot API. Manage rental properties, contacts, showings, conversations, and more from any AI assistant.8 npm1MIT- AlicenseNot gradedqualityDmaintenanceMCP server for Airbnb — lets AI agents search listings, check availability, manage reservations, and book stays via browser automation.8 npmMIT
- FlicenseAqualityDmaintenanceA FastMCP server that exposes hospitality data — accommodations, events, and gastronomy — through a unified MCP interface, supporting multiple worlds for real or fictional data.5-