pyRevit-MCP

Ein MCP-Server, der ein geöffnetes Revit-Modell über natürliche Sprache bedienbar macht. Elemente finden und filtern, Parameter lesen und schreiben, Ergebnisse im Modell einfärben, Tabellen in beide Richtungen bewegen, Screenshots in den Chat holen. Ohne Klickweg durch die Oberfläche.

An MCP server that makes an open Revit model operable through natural language. Find and filter elements, read and write parameters, colour results in the model, move tables both ways, pull screenshots into the chat. Without clicking through the interface.

21Toolstools
3Ebenenlayers
3.982Zeilen Codelines of code
15dokumentierte Fallendocumented pitfalls
11.08.2026Standas of

Was das ist

Ein Gebäudemodell beantwortet normalerweise nur, was man anklickt. pyRevit-MCP macht es ansprechbar: Wer im Chat schreibt „setz bei Tür 700932 den Zargentyp auf Stahl 60 Minuten“, bekommt keine Anleitung, wo der Wert im Eigenschaftenfenster steht. Die Tür trägt den Wert danach, und das Änderungsprotokoll von Revit weiß davon.

MCP in einem Absatz

MCP steht für Model Context Protocol. Es ist die vereinbarte Sprache, in der ein Chat-Modell erfährt, welche Werkzeuge es hat und wie es sie aufruft. Ein MCP-Server meldet beim Start eine Liste von Tools an, jedes mit Namen, Parametern und einer Beschreibung in Prosa. Das Modell liest diese Liste, entscheidet selbst, welches Tool zur Anfrage passt, füllt die Parameter und ruft auf. Der Server macht die Arbeit und gibt das Ergebnis zurück.

Der praktische Unterschied zu einem Makro: Ein Makro muss man kennen, finden und mit den richtigen Argumenten starten. Ein Tool beschreibt sich selbst, und das Modell wählt es aus. Genau daran hängt die Qualität der Tool-Beschreibungen. Sie sind kein Kommentar für Entwickler, sie sind die Bedienungsanleitung für das Modell.

Wofür der Server gebaut wurde

Der Anlass war ein Demo-Video. Ein kurzer Split-Screen, links ein Chat, rechts Revit, um zu zeigen, dass ein Gebäudemodell auf Sprache reagiert. Der Zweck ist breiter geworden. Was hier entsteht, ist ein eigener, schlanker Zugang zu Revit, der Schritt für Schritt wächst und in Kundengesprächen genauso taugt wie in der eigenen Arbeit. Das Video war der Auslöser, nicht das Ziel.

Daraus folgt eine Entscheidung, die sich durch die ganze Doku zieht: Es wird nur gebaut, was verstanden ist. Der Server ist auf einundzwanzig Tools gewachsen, und jedes davon ist Ende zu Ende getestet, bevor es hier steht. Einundzwanzig Tools, die zuverlässig laufen, sind mehr wert als fünfzig, von denen die Hälfte in einer Vorführung stehenbleibt.

Begriff pyRevit Routes

pyRevit ist eine freie Erweiterung für Revit, mit der sich Python-Code direkt in Revit ausführen lässt. Routes ist ein Teil davon: ein kleiner Webserver im Revit-Prozess, der aus Python-Funktionen REST-Endpunkte macht. Damit lässt sich Revit von außen ansprechen, ohne ein eigenes Add-in in C# zu schreiben und zu signieren. Routes ist als Beta gekennzeichnet, und das merkt man, siehe Fallen im Betrieb.

What this is

A building model usually only answers what you click on. pyRevit-MCP makes it conversational: type “set the frame type on door 700932 to steel, 60 minutes” into the chat and you do not get directions to the right field in the properties panel. The door carries the value afterwards, and Revit’s undo history knows about it.

MCP in one paragraph

MCP stands for Model Context Protocol. It is the agreed language in which a chat model learns which tools it has and how to call them. An MCP server announces a list of tools at startup, each with a name, parameters and a description in prose. The model reads that list, decides for itself which tool fits the request, fills in the parameters and calls it. The server does the work and returns the result.

The practical difference from a macro: a macro has to be known, found and started with the right arguments. A tool describes itself, and the model picks it. That is exactly what the quality of the tool descriptions hangs on. They are not comments for developers, they are the manual for the model.

What the server was built for

The occasion was a demo video. A short split screen, chat on the left, Revit on the right, to show that a building model responds to language. The purpose has grown wider. What is taking shape here is an own, lean way into Revit that grows step by step and serves customer conversations as well as everyday work. The video was the trigger, not the goal.

One decision follows from that and runs through the whole document: only what is understood gets built. The server has grown to twenty-one tools, and each of them is tested end to end before it appears here. Twenty-one tools that work reliably are worth more than fifty of which half stall during a demo.

The term pyRevit Routes

pyRevit is a free extension for Revit that lets you run Python code inside Revit. Routes is a part of it: a small web server inside the Revit process that turns Python functions into REST endpoints. That makes Revit addressable from the outside without writing and signing a C# add-in of your own. Routes is labelled beta, and it shows, see pitfalls in daily use.

Architektur

Drei Ebenen, und der interessante Teil ist der Weg zwischen ihnen. Oben zwei Zugänge: ein Browser mit der eigenen Chat-Oberfläche und, davon unabhängig, jeder beliebige MCP-Client wie Claude Code. Beide landen in derselben HTTP-Brücke. Diese spricht mit dem Webserver, der in Revit selbst läuft. Nichts davon geht über das Netz, alles läuft auf einem Rechner.

Architecture

Three layers, and the interesting part is the path between them. Two entry points on top: a browser running the project’s own chat interface and, independently of it, any MCP client such as Claude Code. Both end up in the same HTTP bridge. That bridge talks to the web server running inside Revit itself. None of this goes over the network; everything runs on one machine.

Browser Browser web/index.html MCP-Client Claude Code, Claude Desktop Claude Code, Claude Desktop HTTP · JSON JSON-RPC über STDIO over STDIO app.py FastAPI · Port 8100 Chat-Schleife · CSV · Undo chat loop · CSV · undo Claude API claude-haiku-4-5 wählt das Tool picks the tool server.py FastMCP · @mcp.tool() 21 Tools 21 tools revit_get() · revit_post() in server.py · Basis-URL · Schrägstrich am Ende · doppelte JSON-Hülle · ein HTTP-Client mit Zeitgrenze inside server.py · base URL · trailing slash · double JSON envelope · one HTTP client with a timeout HTTP · localhost:48884 pyRevit Routes Webserver im Revit-Prozess · IronPython 2.7 · Beta web server inside the Revit process · IronPython 2.7 · beta status.py elements.py parameters.py views.py selection.py model.py Revit API Revit 2027 · geöffnetes Dokument · Transaktion beim Schreiben Revit 2027 · open document · transaction on write
Der Weg einer Anfrage. Aus dem Browser kommt Text, app.py legt ihn Claude mit der Tool-Liste vor, Claude wählt ein Tool, app.py ruft die Brücke, die Brücke ruft die Route in Revit. Der MCP-Client rechts geht denselben Weg ab der Brücke, nur ohne eigene Oberfläche und ohne eigenen API-Schlüssel. Zurück geht immer JSON.
The path of a request. Text arrives from the browser, app.py presents it to Claude together with the tool list, Claude picks a tool, app.py calls the bridge, the bridge calls the route inside Revit. The MCP client on the right takes the same path from the bridge onwards, only without an interface and without an API key of its own. What comes back is always JSON.

Warum die Brücke nur einmal existiert

app.py importiert revit_get und revit_post aus server.py. Das ist die eine Entscheidung, die diese Architektur zusammenhält. Sonst gäbe es zwei Fassungen desselben HTTP-Codes, und jede Eigenheit von pyRevit müsste zweimal richtig behandelt werden. Die doppelte JSON-Hülle ist genau so eine Eigenheit, und sie fällt nur an einer Stelle an.

Der Preis ist eine ungewöhnliche Abhängigkeit: Das Web-Backend hängt am MCP-Server, obwohl es MCP gar nicht spricht. Das ist bewusst so, solange die Brücke klein bleibt. Wächst sie, gehört sie in eine eigene Datei.

Was die Brücke abnimmt

Vier Dinge, die sonst in jedem Aufruf einzeln stünden und dort jedes Mal neu falsch werden könnten.

Die doppelte JSON-Hülle

pyRevit serialisiert die Antwort des Handlers ein zweites Mal. Beim Aufrufer kommt JSON an, das eine JSON-Zeichenkette enthält. _decode() erkennt beide Formen und packt aus.

Adresse und Schrägstrich

Die Basis-URL steht an einer Stelle, und jeder Endpunkt endet auf einem Schrägstrich. Ohne ihn antwortet pyRevit mit 404, was wie eine fehlende Route aussieht.

Ein Client statt vieler

Ein httpx.AsyncClient wird angelegt und wiederverwendet, mit 30 Sekunden Zeitgrenze. Große Modelle brauchen die, besonders bei Räumen.

Fehler als Antwort, nicht als Absturz

dispatch_tool() in app.py fängt jede Ausnahme und gibt sie als {"error": …} zurück. Das Modell liest den Text und stellt eine Rückfrage, statt dass das Gespräch abbricht.

Why the bridge exists only once

app.py imports revit_get and revit_post from server.py. That is the one decision holding this architecture together. Otherwise there would be two versions of the same HTTP code, and every pyRevit quirk would have to be handled correctly twice. The double JSON envelope is exactly such a quirk, and it is dealt with in one place only.

The price is an unusual dependency: the web backend hangs off the MCP server although it does not speak MCP at all. That is deliberate as long as the bridge stays small. Once it grows, it belongs in a file of its own.

What the bridge takes off your hands

Four things that would otherwise sit in every single call and could go wrong afresh in each one.

The double JSON envelope

pyRevit serialises the handler’s answer a second time. What arrives is JSON containing a JSON string. _decode() recognises both shapes and unwraps.

Address and trailing slash

The base URL lives in one place, and every endpoint ends in a slash. Without it pyRevit answers 404, which looks exactly like a missing route.

One client instead of many

A single httpx.AsyncClient is created and reused, with a 30 second timeout. Large models need it, rooms in particular.

Errors as answers, not as crashes

dispatch_tool() in app.py catches every exception and returns it as {"error": …}. The model reads the text and asks a follow-up question instead of the conversation breaking off.

Was es kann

Einundzwanzig Tools in sechs Gruppen. Vollständige Parameterlisten stehen in der Tool-Referenz, hier steht, wofür sie gut sind.

Orientierung 6 Tools

get_status ist der erste Aufruf jeder Sitzung: Läuft Revit, welches Dokument ist offen. get_model_info liefert den Überblick über das Modell samt Elementzahlen der wichtigsten Kategorien, get_levels und get_views die Geschosse und Ansichten, get_categories alle Kategorien des Modells, auch lokalisierte. list_parameters beantwortet die Frage vor jedem Filter und jedem Schreibvorgang: Wie heißt der Parameter genau, ist er Instanz oder Typ, und ist er überhaupt beschreibbar.

Elemente finden 3 Tools

find_elements ist das Arbeitstier: Es filtert eine Kategorie nach Geschoss und nach einem Parameterwert (gleich, enthält, leer, größer, kleiner) und liefert die gewünschten Parameter im selben Aufruf mit. „Alle Türen unter 1,20 Meter Breite“ ist ein einziger Aufruf, nicht zweihundert. get_elements holt die einfache Liste einer Kategorie, get_element_properties alle Werte eines einzelnen Elements, Instanz- und Typ-Parameter getrennt ausgewiesen, bei Türen samt Raumzuordnung.

Typ-Parameter inklusive

Die Suche fällt automatisch von der Instanz auf den Elementtyp zurück. Die Breite einer Tür ist bei Standardfamilien ein Typ-Parameter; ohne diesen Fallback findet ein Breitenfilter schlicht nichts. Werte kommen als Anzeigewert in Projekteinheiten zurück, so wie das Eigenschaftenfenster sie zeigt.

Parameter schreiben 2 Tools

set_parameter setzt einen Wert, set_parameters viele auf einmal in einer einzigen Revit-Transaktion: ein Roundtrip, ein Eintrag im Änderungsprotokoll, ein Strg+Z. Beide lesen vor dem Schreiben den alten Wert und geben ihn zurück, darauf baut die Undo-Funktion der Weboberfläche auf. Trifft ein Schreibvorgang einen Typ-Parameter, meldet die Antwort, wie viele Instanzen die Änderung betrifft, samt Warnung.

Sichtbar machen 5 Tools

Das, was in einer Vorführung zählt: override_graphics färbt Elemente in der aktiven Ansicht, isolate_elements blendet alles andere aus, color_by_parameter färbt eine ganze Kategorie nach ihren Parameterwerten, eine Farbe pro Wert, mit Legende in der Antwort. reset_view nimmt alles davon zurück. export_image exportiert die aktive Ansicht als PNG, in der Weboberfläche erscheint das Bild direkt im Chat.

Auswahl und Ansicht 3 Tools

get_selection liest, was in Revit markiert ist, und macht „diese Wände hier“ zu einer beantwortbaren Frage. set_selection markiert Elemente aus dem Chat heraus, set_active_view wechselt die Ansicht.

Modellpflege 2 Tools

get_warnings gruppiert die Warnungen des Modells nach Typ, mit Anzahl und betroffenen Elementen. Zusammen mit override_graphics wird daraus eine Modellprüfung im Chat: „Wie gesund ist dieses Modell?“ save_document speichert, bei Arbeitsteilung wird mit dem Zentralmodell synchronisiert. Ohne Speichern leben alle Änderungen nur im Arbeitsspeicher.

What it can do

Twenty-one tools in six groups. Full parameter lists are in the tool reference; this section says what they are good for.

Orientation 6 tools

get_status is the first call of any session: is Revit running, which document is open. get_model_info gives the model overview including element counts for common categories, get_levels and get_views the storeys and views, get_categories every category in the model, localized names included. list_parameters answers the question that precedes every filter and every write: what is the parameter called exactly, is it instance or type, and is it writable at all.

Finding elements 3 tools

find_elements is the workhorse: it filters a category by level and by a parameter value (equals, contains, empty, greater, less) and returns the requested parameters in the same call. “All doors below 1.2 metres of width” is a single call, not two hundred. get_elements fetches the plain list of a category, get_element_properties every value of a single element, instance and type parameters reported separately, with room assignment for doors.

Type parameters included

Lookups fall back from the instance to the element type automatically. A door’s width is a type parameter in standard families; without that fallback a width filter simply finds nothing. Values come back as display values in project units, the way the properties panel shows them.

Writing parameters 2 tools

set_parameter sets one value, set_parameters many at once inside a single Revit transaction: one round trip, one entry in the undo history, one Ctrl+Z. Both read the previous value before writing and return it; the undo function of the web interface builds on that. When a write lands on a type parameter, the response reports how many instances the change affects, with a warning.

Making it visible 5 tools

What counts in a demo: override_graphics colours elements in the active view, isolate_elements hides everything else, color_by_parameter colours a whole category by its parameter values, one colour per value, legend included in the response. reset_view takes all of it back. export_image exports the active view as a PNG; in the web interface the picture appears right in the chat.

Selection and view 3 tools

get_selection reads what is selected in Revit and turns “these walls here” into an answerable question. set_selection selects elements from the chat, set_active_view switches the view.

Model health 2 tools

get_warnings groups the model’s warnings by type, with counts and the affected elements. Together with override_graphics that makes a model check inside the chat: “how healthy is this model?” save_document saves; workshared models synchronize with central. Without saving, every change lives only in memory.

Die Web-Chat-UI

Eine einzelne HTML-Datei, ausgeliefert von app.py unter http://localhost:8100. Kein Framework, kein Bauschritt, keine Abhängigkeit aus dem Netz. Wer die Datei öffnet, sieht alles: Aufbau, Gestaltung und Verhalten stehen untereinander in derselben Datei.

Sie existiert, weil ein MCP-Client im Terminal für eine Vorführung nicht taugt. Neben einem Revit-Fenster braucht es etwas, das ohne Erklärung lesbar ist und nicht nach Entwicklerwerkzeug aussieht. Genau das ist ihr einziger Zweck, und deshalb ist sie bewusst zurückhaltend gestaltet. Der Star ist das Modell in Revit, nicht die Oberfläche.

Was beim Absenden passiert

Die Nachricht geht an /chat. app.py hängt sie an den Gesprächsverlauf und schickt alles zusammen mit der Tool-Liste an Claude. Antwortet Claude mit stop_reason: tool_use, ruft dispatch_tool die betroffene Route in Revit auf, hängt das Ergebnis als Werkzeugantwort an den Verlauf und fragt Claude erneut. Diese Schleife läuft, bis Claude reinen Text zurückgibt. Erst dann geht die Antwort an den Browser, zusammen mit der Spur der benutzten Tools.

Der Browser zeigt diese Spur als kleine Marken über der Antwort, etwa get_elements · Doors. Das ist kein Beiwerk. In einer Vorführung ist es der Beleg, dass tatsächlich das Modell befragt wurde und nicht das Sprachmodell geraten hat.

CSV-Upload

Eine Tabelle lässt sich per Ziehen und Ablegen ins Fenster werfen oder über die Büroklammer auswählen. Pflicht ist eine Spalte element_id, jede weitere Spalte gilt als Parametername. Das Frontend liest die Datei selbst, erkennt Komma oder Semikolon als Trenner und zeigt eine Vorschau der ersten zehn Zeilen als Karte im Gesprächsverlauf. Erst ein Klick auf Write to Revit schickt die Daten an /upload-csv, das alle Werte gebündelt über set_parameters schreibt: ein Roundtrip, eine Transaktion, ein Eintrag im Änderungsprotokoll von Revit.

Dieser Weg läuft bewusst am Sprachmodell vorbei. Bei zweihundert Zeilen wäre der Umweg über den Chat langsam, teuer und fehleranfällig, ohne dass das Modell irgendetwas beitragen würde. Die Karte zeigt am Ende, wie viele Werte geschrieben wurden und wie viele gescheitert sind. Den Rückweg gibt es auch: export_csv macht aus einem Filterergebnis eine CSV-Datei mit Download-Knopf im Chat. Exportieren, in Excel ändern, wieder hochladen.

Undo

Jeder erfolgreiche Schreibvorgang liefert den alten Wert mit. app.py sammelt die alten Werte eines Vorgangs zu einem Stapeleintrag und merkt sich die Reihenfolge. Der Knopf im Chat schickt den Index an /undo, das die alten Werte in derselben Weise zurückschreibt, über dieselbe Route und damit erneut in einer Transaktion.

Das ersetzt nicht das Rückgängigmachen in Revit, es ergänzt es. Der Nutzen liegt darin, dass der Rückweg im Chat sichtbar ist, direkt neben der Änderung, die man gerade bereut.

Vorschläge und Seitenleiste

Der Systemprompt verlangt am Ende jeder Antwort zwei bis drei Zeilen, die mit SUGGEST: beginnen. Das Frontend schneidet sie aus dem Text heraus und macht Knöpfe daraus. Ein Klick schickt den Vorschlag als nächste Nachricht. In einer Vorführung nimmt das die Denkpause weg, in der man sonst überlegt, was man als Nächstes fragt.

Die Seitenleiste trägt eine Tool-Referenz, die zur Laufzeit vom Endpunkt /tools geladen wird: alle Tools mit Beschreibung und je einem klickbaren Beispielsatz, der das Eingabefeld füllt, ohne abzuschicken. Weil die Liste aus dem Code kommt, kann sie nicht veralten.

Screenshots aus export_image erscheinen als Bild direkt in der Antwortblase, CSV-Exporte als Download-Knopf. Beides kommt aus der Tool-Spur des Backends, das Sprachmodell muss keine Links in den Text schreiben.

Ein Gespräch für alle

conversation und undo_stack sind Variablen auf Modulebene, keine Sitzungsdaten. Zwei geöffnete Browserfenster teilen sich denselben Verlauf und denselben Undo-Stapel. Für eine Vorführung an einem Rechner ist das kein Problem, für mehrere Benutzer ist es keine Grundlage. /reset leert beides.

The web chat UI

A single HTML file, served by app.py at http://localhost:8100. No framework, no build step, no dependency from the network. Open the file and you see everything: structure, styling and behaviour sit one below the other in the same file.

It exists because an MCP client in a terminal is no good for a demo. Next to a Revit window you need something readable without explanation that does not look like a developer tool. That is its only purpose, which is why it is deliberately understated. The star is the model in Revit, not the interface.

What happens when you hit send

The message goes to /chat. app.py appends it to the conversation and sends everything to Claude together with the tool list. If Claude answers with stop_reason: tool_use, dispatch_tool calls the matching route inside Revit, appends the result to the conversation as a tool result and asks Claude again. That loop runs until Claude returns plain text. Only then does the answer go back to the browser, together with the trace of the tools used.

The browser shows that trace as small chips above the answer, for instance get_elements · Doors. This is not decoration. In a demo it is the proof that the model was actually queried rather than the language model guessing.

CSV upload

A table can be dropped into the window or picked through the paperclip. A column named element_id is mandatory; every other column is treated as a parameter name. The frontend reads the file itself, detects comma or semicolon as the separator and shows a preview of the first ten rows as a card in the conversation. Only a click on Write to Revit sends the data to /upload-csv, which writes all values in one bundle through set_parameters: one round trip, one transaction, one entry in Revit’s undo history.

This path deliberately bypasses the language model. With two hundred rows, the detour through the chat would be slow, expensive and error-prone without the model contributing anything. When it finishes, the card reports how many values were written and how many failed. The way back exists too: export_csv turns a filter result into a CSV file with a download button in the chat. Export, edit in Excel, upload again.

Undo

Every successful write returns the previous value. app.py collects the previous values of one operation into a stack entry and remembers the order. The button in the chat sends the index to /undo, which writes the old values back the same way, through the same route and therefore inside a transaction again.

This does not replace undo in Revit, it complements it. The value lies in the way back being visible in the chat, right next to the change you have just come to regret.

Suggestions and sidebar

The system prompt asks for two or three lines beginning with SUGGEST: at the end of every answer. The frontend cuts them out of the text and turns them into buttons. A click sends the suggestion as the next message. In a demo that removes the pause in which you would otherwise be thinking about what to ask next.

The sidebar carries a tool reference loaded at runtime from the /tools endpoint: every tool with its description and one clickable example sentence that fills the input field without sending. Because the list comes from the code, it cannot go stale.

Screenshots from export_image appear as pictures right in the answer bubble, CSV exports as a download button. Both come from the backend’s tool trace; the language model never has to write links into its text.

One conversation for everybody

conversation and undo_stack are module-level variables, not session data. Two open browser windows share the same history and the same undo stack. For a demo on one machine that is fine; as a basis for several users it is not. /reset clears both.

Einrichten und prüfen

Voraussetzungen

Revit 2027 mit einem geöffneten Modell, pyRevit installiert, die Extension registriert. Ohne geöffnetes Dokument geben die Routen zwar Antwort, aber document_title ist leer und jede Abfrage läuft ins Nichts.

Extension einbinden

Der Ordner pyrevit-mcp.extension wird in den pyRevit-Einstellungen unter den benutzerdefinierten Erweiterungsordnern eingetragen. Danach Revit starten. Beim Start läuft startup.py, registriert die sechs Module und schreibt ins pyRevit-Log, wie viele geladen wurden und wie viele gescheitert sind.

Verbindung prüfen

http://localhost:48884/pyrevit_mcp/status/

Im Browser aufrufen. Kommt JSON mit "status": "active" und dem Namen des offenen Dokuments zurück, steht die Grundlage. Kommt nichts, hilft kein weiterer Schritt auf der Python-Seite. Der Schrägstrich am Ende ist Pflicht.

Weboberfläche starten

uv run uvicorn app:app --port 8100

Danach http://localhost:8100 im Browser. Der API-Schlüssel steht in .env als ANTHROPIC_API_KEY und wird mit override=True geladen, damit ein Schlüssel auf Betriebssystemebene ihn nicht überdeckt.

Als MCP-Server registrieren

claude mcp add pyrevit -s user -- uv --directory "C:\path\to\pyrevit-mcp" run server.py

Der Server startet danach mit jeder Chat-Sitzung, es läuft kein Dienst im Hintergrund. Mit /mcp lässt sich prüfen, ob er verbunden ist und wie viele Tools er meldet. Es müssen einundzwanzig sein. Im Projektordner übernimmt die eingecheckte .mcp.json dieselbe Registrierung automatisch. Für die Weboberfläche wird beides nicht gebraucht, sie ist der zweite, unabhängige Zugang.

Prüfen nach jedem Kaltstart

powershell -File scripts\restart_revit.ps1   # Kaltstart automatisiert
uv run python test_routes.py                 # 24 Prüfungen gegen alle Routen
uv run python test_mcp.py                    # MCP-Transport über stdio

Das Restart-Skript liest den Pfad des offenen Modells aus der Status-Route, beendet Revit, öffnet dasselbe Modell neu und wartet, bis die Routen wieder antworten. test_routes.py prüft danach jede Route Ende zu Ende, test_mcp.py denselben Server über den MCP-Transport, also den Weg, den ein echter MCP-Client nimmt. Beide zusammen sind die Antwort auf die teuerste Eigenschaft dieser Architektur: Ein Extension-Fehler zeigt sich erst nach einem Kaltstart, also soll ein Kaltstart so billig wie möglich sein.

Modell durchzählen

uv run python explore_model.py

Geht alle fünfzehn Kategorien durch, zählt sie und zeigt anschließend die Parameter einer beliebigen Wand. Der schnellste Weg, ein unbekanntes Modell einzuschätzen und die Parameternamen zu sehen, mit denen man später schreiben will.

Revit kalt neu starten, nie Reload

Nach jeder Änderung unter pyrevit-mcp.extension/ muss Revit vollständig beendet und neu gestartet werden. Der Routes-Server ist Beta und recycelt beim Neuladen seinen Thread, was Revit ohne Meldung beendet. Das kostet pro Durchgang zwei bis drei Minuten und ist der Grund, warum Änderungen an der Extension teurer sind als Änderungen an server.py oder app.py.

Setup and checks

Prerequisites

Revit 2027 with a model open, pyRevit installed, the extension registered. Without an open document the routes do answer, but document_title is empty and every query runs into nothing.

Registering the extension

The folder pyrevit-mcp.extension is added to the custom extension directories in the pyRevit settings. Then start Revit. On startup startup.py runs, registers the six modules and writes to the pyRevit log how many loaded and how many failed.

Checking the connection

http://localhost:48884/pyrevit_mcp/status/

Open it in a browser. If JSON comes back with "status": "active" and the name of the open document, the foundation is in place. If nothing comes back, no further step on the Python side will help. The trailing slash is mandatory.

Starting the web interface

uv run uvicorn app:app --port 8100

Then http://localhost:8100 in a browser. The API key lives in .env as ANTHROPIC_API_KEY and is loaded with override=True, so that a key set at operating-system level cannot shadow it.

Registering as an MCP server

claude mcp add pyrevit -s user -- uv --directory "C:\path\to\pyrevit-mcp" run server.py

The server then starts with every chat session; there is no background service. Use /mcp to check that it is connected and how many tools it reports. It must be twenty-one. Inside the project folder, the checked-in .mcp.json performs the same registration automatically. The web interface needs neither; it is the second, independent way in.

Checking after every cold restart

powershell -File scripts\restart_revit.ps1   # scripted cold restart
uv run python test_routes.py                 # 24 checks against every route
uv run python test_mcp.py                    # MCP transport over stdio

The restart script reads the open model’s path from the status route, shuts Revit down, reopens the same model and waits until the routes answer again. test_routes.py then checks every route end to end, test_mcp.py the same server over the MCP transport, the path a real MCP client takes. Together they answer the most expensive property of this architecture: an extension bug only shows after a cold restart, so a cold restart should be as cheap as possible.

Taking stock of a model

uv run python explore_model.py

Walks all fifteen categories, counts them and then prints the parameters of an arbitrary wall. The fastest way to size up an unfamiliar model and to see the parameter names you will later want to write to.

Restart Revit cold, never reload

After every change under pyrevit-mcp.extension/ Revit has to be shut down completely and started again. The Routes server is beta and recycles its thread on reload, which terminates Revit without a message. That costs two to three minutes per iteration and is the reason changes to the extension are more expensive than changes to server.py or app.py.

Fallen im Betrieb

Fünfzehn Punkte, die einmal Zeit gekostet haben, jeder mit Symptom, Ursache und Regel.

Der Routes-Server verträgt kein Neuladen

Ein Klick auf Reload in pyRevit beendet Revit. Kein Dialog, kein Logeintrag, das Fenster ist weg. Routes ist Beta und recycelt beim Neuladen den Server-Thread, während der alte den Port noch hält. Die einzige Abhilfe ist ein kalter Neustart nach jeder Änderung an der Extension. Wer das nicht weiß, sucht den Fehler im eigenen Code, und dort ist er nicht.

Der Schrägstrich am Ende

/status/ antwortet, /status gibt 404. Das gilt für jede Route, auch für die mit Parameter. Der Fehler sieht aus wie eine nicht registrierte Route und ist keiner.

Die Antwort steckt zweimal in JSON

Ein Handler gibt eine JSON-Zeichenkette zurück, pyRevit hält das für den Rückgabewert und serialisiert ihn erneut. Beim Aufrufer kommt eine Zeichenkette an, wo ein Objekt erwartet wird, und der Fehler taucht erst weit später beim Feldzugriff auf. _decode() in server.py deckt beide Formen ab.

Ein Collector lässt sich nur einmal durchlaufen

Wer erst zählt und dann über denselben FilteredElementCollector iteriert, bekommt eine leere Liste. Für Gesamtzahl und Elementliste braucht es zwei frische Collector. Genau daher stammen die beiden Zahlen total_count und count in der Antwort.

ElementId hat sich mit Revit 2024 geändert

Bis Revit 2023 nahm der Konstruktor eine normale ganze Zahl, ab 2024 erwartet er System.Int64. Beim Lesen dasselbe umgekehrt: Value ist neu, IntegerValue alt. utils.py versucht jeweils den neuen Weg zuerst und fällt zurück, damit derselbe Code über mehrere Revit-Versionen läuft.

Ein Modul, das nicht in startup.py steht, existiert nicht

Routen registrieren sich nicht selbst. Fehlt der Eintrag in der Liste, wird die Datei nie importiert, die Route gibt 404, und im Log steht nichts, weil nichts fehlgeschlagen ist. Die Schleife fängt Fehler pro Modul ab, ein kaputtes Modul reißt die anderen also nicht mit. Es fehlt nur still.

HTTP 408 heißt selten Timeout

pyRevit gibt den Status 408 auch dann zurück, wenn ein Handler eine Exception wirft. Der Response-Body enthält die echte Fehlermeldung samt Traceback, aber wer bei 408 nur an Timeouts denkt, liest ihn nie und sucht am falschen Ort. Drei Routen fielen so aus, nach jeweils gut zwei Sekunden, und die Ursache war eine UnicodeDecodeError.

json.dumps stürzt an Umlauten ab

IronPythons JSON-Encoder scheitert im Standardmodus an Nicht-ASCII-Zeichen aus der Revit-API, etwa einem „ü“ im Viewnamen „Lüftung 3.OG“. Tückisch daran: Routen funktionieren so lange, wie zufällig keine Umlaute in den Daten stecken, und fallen dann modellabhängig um. Deshalb serialisieren alle Module über dumps() aus utils.py, nie über json.dumps direkt.

Räume sind anders

el.Name wirft bei Räumen eine Exception, der Name steht im Parameter ROOM_NAME. Und ihr Geschoss verrät ein Raum nicht über LevelId, sondern über die Property Level. Beides fiel erst im Probelauf am Klinikmodell auf: Der Geschossfilter fand null Räume, und ohne Filter stürzte die Route ab. element_name() und die erweiterte Geschossauflösung decken beide Wege ab.

LookupParameter findet nur Instanz-Parameter

Typ-Parameter liegen am Elementtyp und nicht an der Instanz. Die Suche gibt None zurück, die Meldung lautet „Parameter nicht gefunden“, und im Eigenschaftenfenster steht er trotzdem sichtbar da. Seit dem 10.08. fängt lookup_param() das ab: erst die Instanz, dann der Typ. Beim Schreiben meldet die Antwort, wie viele Instanzen ein Typ-Parameter betrifft.

Weitere PunkteKurz
Kein BOM am DateianfangIronPython 2.7 scheitert daran. Stattdessen # -*- coding: utf-8 -*- in Zeile 1
doc wird injiziertErstes Argument jedes Handlers, steht nicht in der URL. Fehlt es in der Signatur, gibt es einen TypeError
StorageType steuert die Konvertierungstr, int oder float vor dem Setzen. Parameter vom Typ ElementId werden abgelehnt und die Transaktion zurückgerollt
load_dotenv(override=True)Ohne das gewinnt ein Schlüssel auf Betriebssystemebene, und man sucht den 401 in .env
Sonnet und Opus geben 429Die Organisation hat Kontingent auf Haiku. Beim Umstellen sieht der Fehler nach einem Problem der eigenen Schleife aus

Pitfalls in daily use

Fifteen points that cost time once, each with symptom, cause and rule.

The Routes server cannot survive a reload

A click on Reload in pyRevit terminates Revit. No dialog, no log entry, the window is gone. Routes is beta and recycles the server thread on reload while the old one still holds the port. The only remedy is a cold restart after every change to the extension. Not knowing this, you hunt the fault in your own code, and it is not there.

The trailing slash

/status/ answers, /status returns 404. That holds for every route, including those with a parameter. The failure looks like an unregistered route and is not one.

The answer is wrapped in JSON twice

A handler returns a JSON string, pyRevit takes that for the return value and serialises it again. What arrives is a string where an object is expected, and the failure only surfaces much later on field access. _decode() in server.py covers both shapes.

A collector can only be walked once

Count first and then iterate over the same FilteredElementCollector and you get an empty list. Total and element list need two fresh collectors. That is exactly where the two numbers total_count and count in the response come from.

ElementId changed with Revit 2024

Up to Revit 2023 the constructor took a plain integer; from 2024 it expects System.Int64. Reading is the mirror image: Value is new, IntegerValue old. utils.py tries the new way first and falls back, so the same code runs across several Revit versions.

A module missing from startup.py does not exist

Routes do not register themselves. Without an entry in the list the file is never imported, the route returns 404, and nothing appears in the log because nothing failed. The loop catches errors per module, so a broken module does not take the others down. It is simply and silently absent.

HTTP 408 rarely means timeout

pyRevit also returns status 408 when a handler throws an exception. The response body carries the real error message with a traceback, but thinking “timeout” at a 408 means never reading it and hunting in the wrong place. Three routes failed exactly that way, after a good two seconds each, and the cause was a UnicodeDecodeError.

json.dumps crashes on umlauts

IronPython’s JSON encoder fails in its default mode on non-ASCII characters from the Revit API, such as the “ü” in a view named “Lüftung 3.OG”. The treacherous part: routes keep working as long as no umlaut happens to be in the data, then fall over depending on the model. That is why every module serialises through dumps() from utils.py, never through json.dumps directly.

Rooms are different

el.Name throws for rooms; the name lives in the ROOM_NAME parameter. And a room does not reveal its storey through LevelId but through the Level property. Both only surfaced in the dry run on the hospital model: the storey filter found zero rooms, and without a filter the route crashed. element_name() and the extended level resolution cover both paths.

LookupParameter only finds instance parameters

Type parameters live on the element type, not on the instance. The lookup returns None, the message reads “parameter not found”, and the properties panel shows it plainly all the same. Since 10 August, lookup_param() catches this: instance first, then the type. On writes, the response reports how many instances a type parameter affects.

Further pointsIn short
No BOM at the start of a fileIronPython 2.7 chokes on it. Use # -*- coding: utf-8 -*- on line 1 instead
doc is injectedFirst argument of every handler, absent from the URL. Leave it out of the signature and you get a TypeError
StorageType drives the conversionstr, int or float before setting. Parameters of type ElementId are rejected and the transaction rolled back
load_dotenv(override=True)Without it an operating-system level key wins, and you hunt the 401 inside .env
Sonnet and Opus return 429The organisation has quota on Haiku. On switching, the error looks like a fault in your own loop

Grenzen

Was heute nicht geht

Bewusst offen

Nichts Erzeugendes

Der Server ändert bestehende Elemente, er legt keine an. Räume, Ebenen, Bauteile, Pläne, nichts davon. Das ist der größte verbleibende Sprung im Funktionsumfang, und er wartet bewusst: Erzeugen braucht einen sauberen Rückweg, den es noch nicht gibt.

Bewusst offen

Typ-Schreiben ohne Trockenlauf

Ein Typ-Parameter ändert alle Instanzen des Typs. Die Antwort warnt und nennt die Zahl der Betroffenen, aber erst nachdem geschrieben wurde. Ein Bestätigungsmodus, der die Folgen zeigt, bevor die Transaktion läuft, fehlt noch. Bis dahin verlangt der Systemprompt, dass das Modell vorher nachfragt.

Technisch

Räume sind langsam

Die Abfrage der Kategorie Räume läuft bei sehr großen Modellen in die 30-Sekunden-Grenze des HTTP-Clients. Ursache ist die Menge an Begrenzungsberechnung, die Revit dabei anstößt. Behelf ist ein niedriges limit oder ein Geschossfilter.

Technisch

Parameter vom Typ ElementId

Ein Parameter, der auf ein anderes Element verweist, lässt sich nicht aus einer Zeichenkette setzen. Der Schreibversuch wird abgelehnt und die Transaktion zurückgerollt. Betrifft unter anderem Ebenenzuordnungen und Materialverweise.

Technisch

Scan-Grenze bei 5.000 Elementen

find_elements und color_by_parameter durchlaufen höchstens 5.000 Elemente pro Aufruf, damit kein Aufruf den HTTP-Client aushungert. Die Antwort meldet, wenn die Grenze griff. Bei einer Kategorie mit mehr Elementen hilft der Geschossfilter.

Architektur

Fünf Stellen pro neuem Tool

Route, gegebenenfalls startup.py, @mcp.tool() in server.py, dazu TOOLS, dispatch_tool und ein Beispiel in app.py. Bei einundzwanzig Tools noch tragbar, aber der richtige Umbau wäre, dass app.py die Tool-Liste aus dem MCP-Server ableitet statt sie zu wiederholen.

Architektur

Eine Sitzung für alle

Gesprächsverlauf, Undo-Stapel, Exporte und Bilder liegen in Modulvariablen. Zwei Browserfenster teilen sich alles, ein Neustart des Backends leert es. Für eine Vorführung an einem Rechner unerheblich, für Mehrbenutzerbetrieb nicht tragfähig.

Architektur

Ein Modell, ein Revit

Es gibt keinen Parameter für Projekt oder Datei. Angesprochen wird, was in Revit gerade offen ist, auf demselben Rechner. Das ist eine bewusste Vereinfachung, keine Fernsteuerung mehrerer Arbeitsplätze.

Ändern geht, Erzeugen nicht

Eine Linie zieht sich durch den gesamten heutigen Umfang: Bestehendes lässt sich lesen und ändern, Neues nicht anlegen. Das ist keine Grenze der Revit-API, sondern eine Entscheidung. Ändern ist mit einem gelesenen alten Wert umkehrbar, Erzeugen nicht, und Löschen schon gar nicht. Solange der Rückweg nicht ebenso sauber gebaut ist wie der Hinweg, bleibt der schreibende Teil klein.

Wohin ausgebaut wird

Offen sind die erzeugenden Operationen, Bauteillisten und ein Bestätigungsmodus für Typ-Schreibvorgänge. Was zuerst kommt, entscheidet der nächste konkrete Anlass, nicht ein Plan.

Was hier nicht steht, ist nicht geprüft

Die Doku beschreibt einundzwanzig Tools, die laufen und von zwei Testsuiten geprüft werden. Alles darüber hinaus ist Absichtserklärung. Ein Katalog, dem man trauen kann, ist mehr wert als ein vollständiger.

Limits

What does not work today

Deliberately open

Nothing that creates

The server modifies existing elements, it does not create any. Rooms, levels, building elements, sheets, none of it. That is the biggest remaining jump in scope, and it waits on purpose: creating needs a clean way back, which does not exist yet.

Deliberately open

Type writes without a dry run

A type parameter changes every instance of its type. The response warns and reports the number affected, but only after the write. A confirmation mode that shows the consequences before the transaction runs is still missing. Until then, the system prompt requires the model to ask first.

Technical

Rooms are slow

Querying the rooms category runs into the HTTP client’s 30 second limit on very large models. The cause is the amount of boundary computation Revit triggers. The workaround is a low limit or a level filter.

Technical

Parameters of type ElementId

A parameter pointing at another element cannot be set from a string. The write is rejected and the transaction rolled back. This affects level assignments and material references, among others.

Technical

Scan cap at 5,000 elements

find_elements and color_by_parameter walk at most 5,000 elements per call so that no call starves the HTTP client. The response reports when the cap was hit. For a category with more elements, the level filter helps.

Architecture

Five places per new tool

The route, possibly startup.py, @mcp.tool() in server.py, plus TOOLS, dispatch_tool and an example in app.py. Still bearable at twenty-one tools, but the right refactoring is for app.py to derive the tool list from the MCP server rather than repeat it.

Architecture

One session for everybody

Conversation, undo stack, exports and images live in module-level variables. Two browser windows share everything, a backend restart clears it. Irrelevant for a demo on one machine, unworkable for multiple users.

Architecture

One model, one Revit

There is no parameter for project or file. Whatever is open in Revit on the same machine is what gets addressed. That is a deliberate simplification, not a remote control for several workstations.

Modifying works, creating does not

One line runs through the entire current scope: what exists can be read and changed, what does not cannot be created. That is not a limit of the Revit API but a decision. A change is reversible once the previous value has been read; creation is not, and deletion certainly is not. As long as the way back is not built as carefully as the way there, the writing part stays small.

Where it grows

What remains open are creating operations, schedules, and a confirmation mode for type writes. What comes first will be decided by the next concrete occasion, not by a plan.

What is not written here has not been proven

This document describes twenty-one tools that work and are checked by two test suites. Everything beyond that is intention. A catalogue you can trust is worth more than a complete one.

Tool-Referenz

Alle einundzwanzig MCP-Tools mit Parametern, Fallen und einem Beispielsatz, der sie auslöst, dazu export_csv, das nur die Weboberfläche kennt. Jedes Tool spricht dieselbe Route an, die auch ein Browser aufrufen könnte, und arbeitet immer gegen das gerade in Revit geöffnete Dokument. Es gibt keinen Parameter für Projekt oder Datei, weil es keine Auswahl gibt: Was offen ist, wird angesprochen.

Tool reference

All twenty-one MCP tools, with parameters, pitfalls and a sample sentence that triggers each one, plus export_csv, which only the web interface knows. Every tool addresses the same route a browser could call, and always works against whichever document is currently open in Revit. There is no parameter for project or file, because there is no choice: whatever is open is what gets addressed.