Das ist das Tool, das Claude Code aufruft, um eine kleine App in einen Cloud-Room in osFoundry zu bringen, so wie der Live-Server es in tools/list auflistet (ohne Token abgerufen, auf das Wesentliche gekürzt):
{ "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"]}] } Daneben gibt es vier weitere Room-Tools: read_room, list_room_apps (limit 1–50), read_room_app_status (braucht room_id und instance_id) und propose_room_repo_app (eine repository_id und eine version_id statt files). Alle drei lesenden Tools sind osfoundry:read und readOnlyHint: true. Beide Vorschlags-Tools sind osfoundry:write und destructiveHint: true, und genau deshalb fragt Claude Code nach, bevor es sie aufruft.
Entscheidend ist das Verb. Das Tool legt im Room nichts an. Es erstellt einen Vorschlag, und erst ein Mensch macht daraus einen Room-Tab. Im Folgenden: was das Schema erzwingt und was es dem Server überlässt, wie schwer ein Vorschlag ist, warum der Idempotency-Key Pflicht ist und wie ein Client auf einen Menschen warten sollte. Gemessen wurde alles mit einem Python-Ersatz für den Client, weil auf diesem Mac kein claude-Binary installiert ist.
Was fängt das Schema ab, und was bleibt dem Server überlassen?
room_proto.py validiert Aufrufe mit jsonschema gegen das oben gezeigte Live-inputSchema (gespeichert als tools-list.json). Zwei weitere Regeln ergeben sich daraus, wie osFoundry Snippet-Vorschläge behandelt. Der Einstiegspunkt der Room-Runtime, src/App.tsx, muss vorhanden sein. Und jeder Pfad muss ein sicherer relativer Pfad sein. Das Skript prüft beides zusätzlich zum Schema:
$ 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 Die Hälfte der fehlerhaften Aufrufe kommt durch das JSON Schema. Pfad-Keys in einem files-Objekt lassen sich mit additionalProperties nicht einschränken. Dafür bräuchte man propertyNames mit einem Pattern, und ein Pattern, das .. nach der Normalisierung ablehnt, wird schnell hässlich. Also sagt das Schema „Map aus Strings“, und der Server beurteilt die Keys. Das additionalProperties: false auf oberster Ebene leistet dagegen echte Arbeit. Ein Modell, das sich "visibility": "public" ausdenkt, bekommt einen Schema-Fehler statt eines stillschweigend ignorierten Felds.
Wer einen Client baut, sollte dieselbe Prüfung vor dem Aufruf machen. Ein abgelehnter Vorschlag kostet einen Roundtrip und, schlimmer noch, die Output-Tokens, um die Dateien noch einmal zu schreiben.
Wie groß ist ein Vorschlag?
Die App im Beispiel ist ein Standup-Timer: src/App.tsx (63 Zeilen) und src/styles.css (14 Zeilen).
== 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) Der JSON-RPC-Envelope legt 11.9% auf den reinen Quellcode drauf, vor allem durch escapte Zeilenumbrüche und Anführungszeichen. Das sind aber nicht die Kosten, auf die es ankommt. Jedes Byte in files ist Modell-Output: Claude Code schreibt die Datei und schreibt sie dann ein zweites Mal als Tool-Argument. Ein Vorschlag mit zwei Dateien sind etwa 1,100 Tokens an Argumenten. Eine App mit 21 Dateien, gebaut durch Wiederholen derselben Komponente, sind 14,143. Gezählt wurde mit tiktoken cl100k_base, dem Tokenizer von OpenAI, die Werte sind also Näherungen.
Das Schema veröffentlicht kein Byte-Limit für files. In der Praxis setzt der Reviewer die Grenze. Die Freigabekarte zeigt jede Datei vollständig unter „Review all proposed files (N)“, jede in einer eigenen Scrollbox. Niemand liest 21 Dateien auf einer Karte. Für alles in dieser Größe passt propose_room_repo_app besser: Es verweist auf eine gepinnte Version eines GitNest-Repositorys im selben Projekt, und die Karte verlinkt auf „Review repository code before approving“, statt den Code einzufügen.
Was garantiert die Review-Schranke?
Die Tool-Beschreibung sagt es ganz direkt: Das Snippet „appears in the cloud Room launcher only after its owner reviews and approves the full code“. Im Room ist der Launcher die Tab-Leiste der Apps. Auf der Karte steht „Approval makes this code available in the Room app launcher. Review the files before approving.“ „Approve and create“ fügt den Tab ein. „Decline“ verwirft ihn. Ein offener Vorschlag kann von Room-Mitgliedern nicht geöffnet werden, ein ungeprüftes Snippet läuft also nie in irgendeiner Session.
Damit landet das Retry-Problem beim Client. Bei MCP über HTTP kann eine Antwort verloren gehen, nachdem der Server schon gehandelt hat. Wenn der Client dann mit einem neuen Key erneut sendet, bekommt der Owner zwei Karten für eine App. room_proto.py bildet das mit einem In-Memory-Server nach, der Vorschläge nach Key speichert, mit 1,000 Vorschlägen und einer Wahrscheinlichkeit von 5%, dass eine Antwort verloren geht:
== 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 Mit einer neuen UUID pro Versuch sind 74 der 1,074 Karten Duplikate, die der Owner erkennen und ablehnen muss. Mit einem aus dem Inhalt abgeleiteten Key, "snip-" + sha256(room_id, name, files)[:32], trägt ein Retry denselben Key, und die Zahl der Duplikate ist 0. Eine bewusste Änderung an den Dateien ergibt einen neuen Key und eine neue Karte, und genau das will man. Das Minimum von 8 Zeichen im Schema hält Keys wie "1" draußen. In keinem der beiden Durchläufe ist vor der Freigabe etwas im Launcher gelandet. Das ist Aufgabe des Servers, nicht des Clients.
Wie sollte Claude Code auf den Owner warten?
initialize liefert "capabilities": {"tools": {"listChanged": false}} und sonst nichts. Es gibt keinen Push à la „dein Vorschlag wurde freigegeben“. Der Client hat drei Möglichkeiten. Er kann den Nutzer bitten, Bescheid zu geben, sobald er freigegeben hat, er kann read_room_app_status pollen (oder list_room_apps, bis die neue Instanz auftaucht), oder er kann poll_resource_events für den Room fortsetzen, das resource_type: "room" akzeptiert. Ohne Token habe ich nicht überprüft, welche Events eine Freigabe dort schreibt, deshalb pollt die Simulation den Status.
Die Eingangsgröße ist die Zeit, die ein Mensch fürs Review braucht. Das Skript zieht sie aus einer Log-Normalverteilung mit einem Median von 6 Minuten und einem Budget von einer Stunde. Das ist eine Annahme, keine Messung, aber die Form (die meisten antworten innerhalb von Minuten, ein paar brauchen eine halbe Stunde) ist der realistische Teil:
== 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 Wer alle 5 Sekunden pollt, bemerkt die Freigabe innerhalb von 5 s, bei 112 Aufrufen pro Vorschlag, und jeder Aufruf ist ein Modell-Turn, der für Tool-Output draufgeht. Ein festes Intervall von 30 Sekunden sind 19 Aufrufe mit 28 Sekunden im schlechtesten Fall. Backoff bis zu einer Obergrenze von 2 Minuten drückt das auf 11 Aufrufe, bei einer p95-Verzögerung von 111 s. 14 der 2,000 Reviews dauerten länger als eine Stunde, und bei denen gaben alle Strategien gleichermaßen auf. In einer interaktiven Session ist die günstigste korrekte Antwort null Aufrufe: den Turn mit „approve it under Connected → Needs your approval and tell me“ beenden und danach einen einzigen list_room_apps-Aufruf machen. Polling lohnt sich nur, wenn Claude Code zwischendurch anderes zu tun hat.
Echte Roundtrips zur Einordnung: je zehn nicht authentifizierte Aufrufe von diesem Mac in Japan an den Live-Endpoint.
$ 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" Etwa 0.6 s pro Aufruf. Eine 5-Sekunden-Schleife verbringt also ein Zehntel ihrer Zeit im Netzwerk, und ein Modell-Turn pro Aufruf kostet deutlich mehr als das.
Was bedeutet „build state“ bei einem Snippet?
read_room_app_status liefert „the safe launcher and latest build state of one app“, ohne Config, Dateien, Logs oder Secrets. Ein Snippet hat keinen Server-Build. Der Room kompiliert die Dateien dort, wo der Tab geöffnet wird: in der Desktop-App in einem sandboxed iframe mit Sucrase, im Browser in einer Sandpack-Sandbox. Der Repository-Weg ist ebenfalls gepinnt, und seine Freigabekarte sagt „This path does not build or deploy a bundle.“ Bei beiden Vorschlagsarten ist der Launcher-Eintrag das, worauf man wartet. Ein Build-State erscheint erst, wenn jemand den Tab deployt, und das passiert in osFoundry.
Warum kommt es nicht an einen lokalen Room heran?
read_room sagt „Local Rooms are unavailable“, und initialize enthält die allgemeine Regel: „Local-only data is unavailable to cloud clients.“ Ein lokaler Room liegt in der eigenen Datenbank der Desktop-App und wird nie synchronisiert. Der MCP-Endpoint ist ein Cloud-Dienst mit einem auf den Workspace beschränkten OAuth-Token und hat keinen Weg zu einem bestimmten Laptop. Um dorthin zu kommen, bräuchte es ein Relay, das die Desktop-App betreibt: pro Gerät und Projekt aktiviert, mit Online/Offline-Status und einer Allowlist von Befehlen. Die Tool-Oberfläche tut nicht so, als wäre es anders. Dieselbe Grenze gilt für Daten innerhalb eines Snippets. Dessen SDK-Speicher bleibt auf dem Gerät der Person, die den Tab nutzt, und keines der lesenden Tools liefert Datenbankinhalte zurück.
Wenn du das selbst verdrahtest
- Validiere Pfad-Keys auf dem Server, auch wenn du ein Schema veröffentlichst. JSON Schema kann „Map aus Strings“ beschreiben, aber nicht „sicherer relativer Pfad“, und in diesem Test war die Hälfte der fehlerhaften Aufrufe ein Pfadproblem.
- Leite den Idempotency-Key aus dem Inhalt ab, nicht aus dem Versuch. Verlorene Antworten kosten dann nichts, und eine echte Änderung bekommt trotzdem ihre eigene Karte.
- Lass die Aufmerksamkeit des Reviewers das Größenlimit bestimmen. Wenn die Karte jede Datei zeigt, halte Snippets bei einer Handvoll Dateien und schicke größere Apps als gepinnte Repository-Version.
- Lass den Agenten standardmäßig nicht auf einen Menschen pollen. Lass ihn seinen Turn beenden und einmal den Status lesen, wenn der Nutzer zurückkommt, und nutze Backoff nur, wenn er anderes zu tun hat.
- Halte das Zurücklesen schlank. Gib Identität und Launcher-Status zurück, nie Config oder App-Daten, damit Status-Abfragen nicht zum Weg werden, zu lesen, was Leute gespeichert haben.
- Schreib in die Tool-Beschreibung, was außer Reichweite ist („Local Rooms are unavailable“), damit das Modell es dem Nutzer sagt, statt es erneut zu versuchen.
Die Room-Seite mit Tab-Leiste und Einstellungen findest du auf der Room-Seite. Zum Protokoll allgemein siehe den MCP-Glossareintrag.
![Industrial Communication — Ch.10: Factory Automation [EN]](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%2Ft1hw2kvh64lkchmsurzd.png)

