DEV Community

Cover image for Qué envía Claude Code cuando propone una app a un Room de osFoundry
BBT for BBT Español

Posted on

Qué envía Claude Code cuando propone una app a un Room de osFoundry

Esta es la herramienta que Claude Code llama para meter una app pequeña en un Room en la nube de osFoundry, tal como la lista el servidor en producción en tools/list (obtenida sin token y recortada a lo que importa):

{
  "name": "propose_room_snippet_app",
  "description": "Prepare a Room snippet with source files. It appears in the cloud Room launcher only after its owner reviews and approves the full code in osFoundry.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "room_id": {"type": "string"},
      "name": {"type": "string", "maxLength": 200},
      "files": {"type": "object", "additionalProperties": {"type": "string"}},
      "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}
    },
    "required": ["room_id", "name", "files", "idempotency_key"],
    "additionalProperties": false
  },
  "annotations": {"readOnlyHint": false, "destructiveHint": true, "openWorldHint": false},
  "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]
}
Enter fullscreen mode Exit fullscreen mode

Junto a ella hay cuatro herramientas más de Room: read_room, list_room_apps (limit 1–50), read_room_app_status (requiere room_id e instance_id) y propose_room_repo_app (un repository_id y un version_id en lugar de files). Las tres de lectura son osfoundry:read y readOnlyHint: true. Las dos de propuesta son osfoundry:write y destructiveHint: true, y eso es lo que hace que Claude Code pida confirmación antes de llamarlas.

La clave está en el verbo. La herramienta no crea nada en el Room. Crea una propuesta, y es una persona quien la convierte en una pestaña del Room. A continuación: qué impone el esquema y qué deja al servidor, cuánto pesa una propuesta, por qué la clave de idempotencia es obligatoria y cómo debería esperar un cliente a que responda un humano. Todo se midió con un sustituto del cliente escrito en Python, porque en este Mac no hay ningún binario claude instalado.

¿Qué detecta el esquema y qué deja en manos del servidor?

room_proto.py valida las llamadas con jsonschema contra el inputSchema real de arriba (guardado como tools-list.json). Otras dos reglas salen de cómo osFoundry trata las propuestas de snippets. El punto de entrada del runtime del Room, src/App.tsx, tiene que estar presente. Y cada ruta tiene que ser una ruta relativa segura. El script aplica ambas encima del esquema:

$ uv run --with jsonschema --with tiktoken python3 room_proto.py
== validation ==
ok               PASS
no App.tsx       REJECT  rule: src/App.tsx missing
absolute path    REJECT  rule: unsafe path '/etc/hosts'
dot-dot path     REJECT  rule: unsafe path 'src/../../x.ts'
backslash path   REJECT  rule: unsafe path 'src\\x.ts'
name 201 chars   REJECT  schema: 'nnnn…' is too long
key 7 chars      REJECT  schema: 'abcdefg' is too short
extra field      REJECT  schema: Additional properties are not allowed ('visibility' was unexpected)
non-string file  REJECT  schema: {'a': 1} is not of type 'string'
caught by published schema: 4, needs server rule: 4
Enter fullscreen mode Exit fullscreen mode

La mitad de las llamadas incorrectas pasan el JSON Schema. Las claves de ruta de un objeto files no se pueden restringir con additionalProperties. Haría falta propertyNames con un patrón, y un patrón que rechace .. después de normalizar se vuelve feo enseguida. Así que el esquema dice "mapa de strings" y el servidor juzga las claves. Eso sí, el additionalProperties: false del nivel superior hace un trabajo real. Un modelo que se inventa "visibility": "public" recibe un error de esquema en lugar de un campo ignorado en silencio.

Si construyes un cliente, haz la misma comprobación antes de llamar. Una propuesta rechazada cuesta un viaje de ida y vuelta y, peor aún, los tokens de salida necesarios para volver a escribir los archivos.

¿Cuánto ocupa una propuesta?

La app del ejemplo es un temporizador para la daily: src/App.tsx (63 líneas) y src/styles.css (14 líneas).

== size ==
src/App.tsx        2021 B   63 lines    583 tok
src/styles.css     1046 B   14 lines    399 tok
source bytes 3067, envelope bytes 3431, escape+frame overhead 11.9%
argument tokens (cl100k) 1141
   2 files: envelope    4463 B,   1394 tok
   6 files: envelope   12919 B,   4078 tok
  21 files: envelope   44639 B,  14143 tok
tools/list 45368 B for 59 tools; the 5 room tools 3482 B (939 tok)
Enter fullscreen mode Exit fullscreen mode

El sobre JSON-RPC añade un 11.9% sobre el código fuente en bruto, sobre todo por saltos de línea y comillas escapados. Ese no es el coste que importa. Cada byte de files es salida del modelo: Claude Code escribe el archivo y luego lo vuelve a escribir como argumento de la herramienta. Una propuesta de dos archivos son unos 1,100 tokens de argumentos. Una app de 21 archivos, hecha repitiendo el mismo componente, son 14,143. Los tokens se cuentan con tiktoken cl100k_base, que es el tokenizador de OpenAI, así que tómalos como aproximados.

El esquema no publica ningún límite de bytes para files. En la práctica, el límite lo pone quien revisa. La tarjeta de aprobación muestra cada archivo completo bajo "Review all proposed files (N)", cada uno en su propia caja con scroll. Nadie lee 21 archivos en una tarjeta. Para algo de ese tamaño encaja mejor propose_room_repo_app: apunta a una versión fijada de un repositorio de GitNest del mismo proyecto, y la tarjeta enlaza a "Review repository code before approving" en lugar de pegar el código.

¿Qué garantiza la revisión?

La descripción de la herramienta lo dice claramente: el snippet "appears in the cloud Room launcher only after its owner reviews and approves the full code". En el Room, el launcher es la barra de pestañas de apps. La tarjeta dice "Approval makes this code available in the Room app launcher. Review the files before approving." "Approve and create" inserta la pestaña. "Decline" la descarta. Los miembros del Room no pueden abrir una propuesta pendiente, así que un snippet sin revisar nunca se ejecuta en la sesión de nadie.

Eso traslada el problema de los reintentos al cliente. MCP sobre HTTP puede perder una respuesta después de que el servidor ya haya actuado. Si el cliente reintenta entonces con una clave nueva, el propietario recibe dos tarjetas para una sola app. room_proto.py lo modela con un servidor en memoria que guarda las propuestas por clave, 1,000 propuestas y un 5% de probabilidad de que se pierda cada respuesta:

== review gate, 1000 proposals, 5% lost responses ==
random   calls 1074  owner cards 1074  duplicates 74  in launcher before approval 0
content  calls 1057  owner cards 1000  duplicates 0  in launcher before approval 0
Enter fullscreen mode Exit fullscreen mode

Con un UUID nuevo por intento, 74 de las 1,074 tarjetas son duplicados que el propietario tiene que reconocer y rechazar. Con una clave derivada del contenido, "snip-" + sha256(room_id, name, files)[:32], un reintento lleva la misma clave y el número de duplicados es 0. Un cambio deliberado en los archivos genera una clave nueva y una tarjeta nueva, que es justo lo que quieres. El mínimo de 8 caracteres del esquema deja fuera claves como "1". En ninguna de las dos ejecuciones llegó nada al launcher antes de la aprobación. Esa parte es trabajo del servidor, no del cliente.

¿Cómo debería esperar Claude Code al propietario?

initialize devuelve "capabilities": {"tools": {"listChanged": false}} y nada más. No hay ningún push del tipo "tu propuesta fue aprobada". El cliente tiene tres opciones. Puede pedirle al usuario que avise cuando haya aprobado, puede hacer polling de read_room_app_status (o de list_room_apps hasta que aparezca la nueva instancia), o puede retomar poll_resource_events para el Room, que acepta resource_type: "room". Sin token no verifiqué qué eventos escribe ahí una aprobación, así que la simulación consulta el estado.

El tiempo de revisión humana es la entrada. El script lo saca de una distribución log-normal con una mediana de 6 minutos y un presupuesto de una hora. Es una suposición, no una medición, pero la forma (la mayoría responde en minutos, unos pocos tardan media hora) es la parte realista:

== polling while the owner reviews (n=2000, median review 6 min, 1 h budget) ==
review delay p50 357s  p95 1816s
fixed 5s                   calls mean  111.8  lag p50   2.4s  p95    4.7s  gave up 14
fixed 30s                  calls mean   19.0  lag p50  14.5s  p95   28.4s  gave up 14
backoff 5s x1.5 cap 120s   calls mean   10.8  lag p50  47.0s  p95  111.0s  gave up 14
backoff 10s x2 cap 300s    calls mean    6.1  lag p50 105.2s  p95  273.8s  gave up 14
Enter fullscreen mode Exit fullscreen mode

Hacer polling cada 5 segundos detecta la aprobación en menos de 5 s, a costa de 112 llamadas por propuesta, y cada llamada es un turno del modelo gastado en salida de herramientas. Un intervalo fijo de 30 segundos son 19 llamadas con un peor caso de 28 segundos. Con backoff hasta un tope de 2 minutos se baja a 11 llamadas, y la latencia p95 es de 111 s. 14 de las 2,000 revisiones tardaron más de una hora, y todas las estrategias se rindieron con ellas por igual. En una sesión interactiva, la respuesta correcta más barata son cero llamadas: terminar el turno con "approve it under Connected → Needs your approval and tell me" (apruébalo en Connected → Needs your approval y avísame) y luego hacer una sola llamada a list_room_apps. El polling solo compensa cuando Claude Code tiene otro trabajo que hacer mientras tanto.

Viajes de ida y vuelta reales, para tener escala: diez llamadas sin autenticar de cada tipo desde este Mac en Japón hasta el endpoint en producción.

$ python3 probe.py
initialize                       HTTP 200     308 B  median    680 ms  min   358  max   968
tools/list                       HTTP 200   45368 B  median    687 ms  min   429  max  1253
tools/call read_room (no token)  HTTP 401     337 B  median    559 ms  min   427  max   847
  www-authenticate: Bearer resource_metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp", error="invalid_token", error_description="Connect osFoundry to continue"
Enter fullscreen mode Exit fullscreen mode

Unos 0.6 s por llamada. O sea, un bucle de 5 segundos pasa una décima parte de su tiempo en la red, y un turno del modelo por llamada cuesta mucho más que eso.

¿Qué significa "build state" en un snippet?

read_room_app_status devuelve "the safe launcher and latest build state of one app", sin config, archivos, logs ni secretos. Un snippet no tiene build en el servidor. El Room compila sus archivos allí donde se abre la pestaña: en un iframe aislado con Sucrase en la app de escritorio y en un sandbox de Sandpack en el navegador. La vía del repositorio también va fijada a una versión, y su tarjeta de aprobación dice "This path does not build or deploy a bundle." En los dos tipos de propuesta, lo que hay que esperar es la entrada en el launcher. Un build state aparece cuando alguien despliega la pestaña, y eso se hace en osFoundry.

¿Por qué no puede llegar a un Room local?

read_room dice "Local Rooms are unavailable", y initialize incluye la regla general: "Local-only data is unavailable to cloud clients." Un Room local vive en la base de datos propia de la app de escritorio y nunca se sincroniza. El endpoint MCP es un servicio en la nube con un token OAuth limitado al workspace, y no tiene ninguna ruta hacia un portátil concreto. Llegar hasta él requeriría un relay gestionado por la app de escritorio: activado por dispositivo y por proyecto, con estado online/offline y una lista de comandos permitidos. La superficie de herramientas no finge lo contrario. El mismo límite se aplica a los datos dentro de un snippet. Su almacenamiento del SDK se queda en el dispositivo de quien usa la pestaña, y ninguna de las herramientas de lectura devuelve contenido de la base de datos.

Si lo montas tú mismo

  • Valida las claves de ruta en el servidor aunque publiques un esquema. JSON Schema puede describir "mapa de strings", pero no "ruta relativa segura", y en esta prueba la mitad de las llamadas incorrectas eran problemas de rutas.
  • Deriva la clave de idempotencia del contenido, no del intento. Así las respuestas perdidas no cuestan nada, y una edición real sigue teniendo su propia tarjeta.
  • Deja que la atención de quien revisa marque el límite de tamaño. Si la tarjeta muestra todos los archivos, limita los snippets a unos pocos y envía las apps más grandes como una versión fijada de un repositorio.
  • No hagas que el agente haga polling esperando a un humano por defecto. Haz que termine su turno y lea el estado una sola vez cuando vuelva el usuario, y usa backoff solo cuando tenga otro trabajo.
  • Mantén mínima la lectura de vuelta. Devuelve identidad y estado del launcher, nunca config ni datos de la app, para que las consultas de estado no se conviertan en una forma de leer lo que la gente ha guardado.
  • Indica en la descripción de la herramienta lo que queda fuera de alcance ("Local Rooms are unavailable"), para que el modelo se lo diga al usuario en lugar de reintentar.

La parte del Room, con la barra de pestañas y los ajustes, está en la página de Room. Para el protocolo en general, consulta la entrada del glosario sobre MCP.

Top comments (0)