AI & MCP Server
RevitPy includes a full AI integration layer that exposes Revit operations through the Model Context Protocol (MCP). This enables AI agents and LLMs to query, analyze, modify, and export Revit model data through a standardized WebSocket interface, with configurable safety guardrails and reusable prompt templates.
Overview
The revitpy.ai module provides four core components:
RevitTools– A tool registry and execution engine that manages tool definitions, validates arguments, dispatches execution, and converts tools to MCP-compatible JSON Schema format.SafetyGuard– A safety policy enforcer that controls which tools an AI agent may execute based on a configurable safety mode, with preview and undo support.PromptLibrary– A Jinja2-based template library for constructing prompts used in LLM interactions, exposed in MCP prompt-list format.McpServer– An asynchronous WebSocket server implementing a subset of MCP, wiring together tools, prompts, and safety controls.
from revitpy.ai import (
McpServer,
RevitTools,
SafetyGuard,
PromptLibrary,
SafetyConfig,
SafetyMode,
McpServerConfig,
ToolCategory,
)
RevitTools
RevitTools is the tool registry. It ships with six built-in tools and allows registering custom ones.
Creating a Registry
from revitpy.ai import RevitTools
# Without application context (built-in handlers return placeholder data)
tools = RevitTools()
# With application context (forwarded to built-in handlers)
tools = RevitTools(context=revit_app)
Built-in Tools
The following tools are registered automatically when a RevitTools instance is created:
| Tool Name | Category | Required Parameters | Optional Parameters | Description |
|---|---|---|---|---|
query_elements |
QUERY |
category (string) |
filter (string, default "") |
Query Revit elements by category and filter |
get_element |
QUERY |
element_id (integer) |
– | Get a single Revit element by ID |
modify_parameter |
MODIFY |
element_id (integer), parameter_name (string), value (string) |
– | Modify a parameter value on a Revit element |
get_quantities |
ANALYZE |
category (string) |
group_by (string, default "type") |
Get quantity takeoff for elements |
validate_model |
ANALYZE |
– | checks (array, default None) |
Run validation checks on the Revit model |
export_data |
EXPORT |
category (string) |
format (string, default "json") |
Export element data to a structured format |
Registering a Custom Tool
Use register_tool to add a tool with a ToolDefinition and a handler callable:
from revitpy.ai import (
RevitTools,
ToolDefinition,
ToolParameter,
ToolCategory,
ParameterType,
)
tools = RevitTools()
tools.register_tool(
definition=ToolDefinition(
name="count_by_level",
description="Count elements on a specific level",
category=ToolCategory.ANALYZE,
parameters=[
ToolParameter(
name="level_name",
type=ParameterType.STRING,
description="Name of the level",
),
ToolParameter(
name="category",
type=ParameterType.STRING,
description="Element category to count",
required=False,
default="all",
),
],
returns_description="Element count per level",
),
handler=my_count_handler,
)
Executing Tools
execute_tool validates required parameters, invokes the handler, and returns a ToolResult:
result = tools.execute_tool("query_elements", {"category": "Walls"})
print(result.status) # ToolResultStatus.SUCCESS
print(result.data) # {"elements": [...], "count": 5, ...}
print(result.execution_time_ms) # 12.3
print(result.error) # None
If the tool name is unknown or required parameters are missing, the result has status ToolResultStatus.ERROR with a descriptive error message. If the handler raises an exception, a ToolExecutionError is raised.
Listing and Inspecting Tools
# List all registered tool definitions
all_tools = tools.list_tools()
# Get a single tool definition by name
defn = tools.get_tool("query_elements")
print(defn.name) # "query_elements"
print(defn.category) # ToolCategory.QUERY
print(defn.description) # "Query Revit elements by category and filter"
print(defn.parameters) # [ToolParameter(...), ...]
Converting to MCP Format
to_mcp_tool_list converts all registered tools to the MCP JSON Schema tool format:
mcp_tools = tools.to_mcp_tool_list()
# Returns a list of dicts, each with "name", "description", and "inputSchema"
Each entry in the list has this shape:
{
"name": "query_elements",
"description": "Query Revit elements by category and filter",
"inputSchema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "Element category (e.g. Walls, Doors)"
},
"filter": {
"type": "string",
"description": "Optional filter expression",
"default": ""
}
},
"required": ["category"]
}
}
SafetyGuard
SafetyGuard validates tool calls against a configurable safety policy to prevent unintended model modifications. It also provides a preview mechanism and an undo stack.
Safety Modes
| Mode | Value | Behavior |
|---|---|---|
SafetyMode.READ_ONLY |
"read_only" |
Blocks all tools with ToolCategory.MODIFY. Query, analyze, and export tools are allowed. |
SafetyMode.CAUTIOUS |
"cautious" |
Allows all tools but flags categories in require_confirmation_for as needing confirmation. This is the default. |
SafetyMode.FULL_ACCESS |
"full_access" |
Allows all tools without restriction, except those in the blocked_tools list. |
SafetyConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
mode |
SafetyMode |
SafetyMode.CAUTIOUS |
The active safety enforcement level |
max_undo_stack |
int |
50 |
Maximum number of entries in the undo stack |
require_confirmation_for |
list[ToolCategory] |
[] |
Categories that require confirmation in CAUTIOUS mode |
blocked_tools |
list[str] |
[] |
Tool names that are always denied regardless of mode |
Creating a SafetyGuard
from revitpy.ai import SafetyGuard, SafetyConfig, SafetyMode, ToolCategory
# Default: CAUTIOUS mode
guard = SafetyGuard()
# READ_ONLY mode -- blocks all modify operations
guard = SafetyGuard(config=SafetyConfig(mode=SafetyMode.READ_ONLY))
# CAUTIOUS with confirmation for modify and export
guard = SafetyGuard(config=SafetyConfig(
mode=SafetyMode.CAUTIOUS,
require_confirmation_for=[ToolCategory.MODIFY, ToolCategory.EXPORT],
blocked_tools=["export_data"],
))
Validating Tool Calls
validate_tool_call returns True when the call is allowed, or raises SafetyViolationError when blocked:
from revitpy.ai import ToolDefinition, ToolCategory
tool = tools.get_tool("modify_parameter")
try:
guard.validate_tool_call(tool, {"element_id": 12345, "parameter_name": "Height", "value": "3.0"})
print("Tool call allowed")
except SafetyViolationError as e:
print(f"Blocked: {e}")
Previewing Changes
preview_changes returns a dry-run summary without applying any changes:
preview = guard.preview_changes(tool, {"element_id": 12345, "parameter_name": "Height", "value": "3.0"})
print(preview)
# {
# "tool": "modify_parameter",
# "category": "modify",
# "arguments": {"element_id": 12345, ...},
# "safety_mode": "cautious",
# "requires_confirmation": True,
# "is_blocked": False,
# }
Undo Stack
The undo stack records operations so they can be rolled back. The stack is bounded by SafetyConfig.max_undo_stack (default 50); the oldest entry is discarded when the limit is reached.
# Push an operation onto the undo stack
guard.push_undo({
"tool": "modify_parameter",
"element_id": 12345,
"parameter_name": "Height",
"old_value": "2.5",
"new_value": "3.0",
})
# Pop and return the most recent undo entry
last = guard.undo_last() # Returns the dict, or None if empty
# Inspect the full stack (returns a copy)
stack = guard.get_undo_stack()
PromptLibrary
PromptLibrary manages Jinja2 templates used to construct prompts for LLM interactions. It ships with five built-in templates and supports adding custom ones at runtime.
Built-in Templates
| Template Name | Variables | Purpose |
|---|---|---|
element_summary |
element_id, name, category, parameters (dict) |
Summarize a Revit element |
quantity_takeoff |
category, group_by (optional), columns (optional) |
Generate a quantity takeoff report |
validation_report |
issues (list of dicts with severity and message) |
Format model validation results |
natural_language_query |
user_query, categories (list) |
Translate natural language to a Revit query |
safety_preview |
tool_name, category, arguments (dict), safety_mode, requires_confirmation |
Preview a tool operation |
Rendering Templates
from revitpy.ai import PromptLibrary
prompts = PromptLibrary()
text = prompts.render(
"element_summary",
element_id=12345,
name="Basic Wall",
category="Walls",
parameters={"Height": "3.0m", "Width": "0.2m"},
)
print(text)
# Summarize the following Revit element:
# - ID: 12345
# - Name: Basic Wall
# - Category: Walls
# - Parameters:
# - Height: 3.0m
# - Width: 0.2m
The render method uses Jinja2 with StrictUndefined, so missing variables raise a PromptError.
Registering Custom Templates
prompts.register_template(
"cost_estimate",
"Estimate the cost of 8 elements "
"at $ per unit.\n"
"Total estimated cost: $8\n",
)
text = prompts.render("cost_estimate", count=50, material="steel beams", unit_price=120)
Listing and Inspecting Templates
# Sorted list of all template names
names = prompts.list_templates()
# Get raw Jinja2 source of a template
source = prompts.get_template("element_summary")
Converting to MCP Format
to_mcp_prompts_list converts templates to MCP-format prompt definitions:
mcp_prompts = prompts.to_mcp_prompts_list()
# Returns a list of dicts with "name", "description", and "arguments"
McpServer
McpServer is an asynchronous WebSocket server that implements a subset of the Model Context Protocol, wiring together RevitTools, SafetyGuard, and PromptLibrary.
McpServerConfig Fields
| Field | Type | Default | Description |
|---|---|---|---|
host |
str |
"localhost" |
Host address to bind to |
port |
int |
8765 |
Port number |
name |
str |
"revitpy-mcp" |
Server name reported during initialization |
version |
str |
"1.0.0" |
Server version reported during initialization |
Creating and Starting a Server
from revitpy.ai import McpServer, RevitTools, McpServerConfig
tools = RevitTools()
server = McpServer(
tools,
config=McpServerConfig(host="0.0.0.0", port=9000),
)
# Start and stop manually
import asyncio
async def main():
await server.start()
# Server is now accepting WebSocket connections
# ... wait or do work ...
await server.stop(timeout=5.0)
asyncio.run(main())
Async Context Manager
McpServer supports async with for automatic lifecycle management:
async def main():
tools = RevitTools()
async with McpServer(tools) as server:
print(f"Server running on {server.config.host}:{server.config.port}")
# Server starts on __aenter__, stops on __aexit__
await asyncio.sleep(3600) # Run for one hour
Injecting Safety and Prompts
Pass custom SafetyGuard and PromptLibrary instances to the server:
from revitpy.ai import (
McpServer,
RevitTools,
SafetyGuard,
SafetyConfig,
SafetyMode,
PromptLibrary,
ToolCategory,
)
guard = SafetyGuard(config=SafetyConfig(
mode=SafetyMode.CAUTIOUS,
require_confirmation_for=[ToolCategory.MODIFY],
))
prompts = PromptLibrary()
prompts.register_template("custom_prompt", "Hello, !")
server = McpServer(
RevitTools(),
safety_guard=guard,
prompt_library=prompts,
)
Supported MCP Methods
The server handles the following JSON-RPC methods over the WebSocket connection:
| Method | Description |
|---|---|
initialize |
Returns protocol version ("2024-11-05"), capabilities, and server info |
tools/list |
Returns all registered tools in MCP JSON Schema format |
tools/call |
Validates the call through the safety guard, then executes the tool |
prompts/list |
Returns all registered prompt templates |
prompts/get |
Renders a prompt template with the supplied arguments |
Server Properties
# Access the active server configuration
config = server.config
print(config.host, config.port)
# View active WebSocket connections
connections = server.connections # Returns a set copy
Enum Reference
ToolCategory
| Member | Value | Description |
|---|---|---|
QUERY |
"query" |
Read-only queries against the Revit model |
MODIFY |
"modify" |
Modifies elements or parameters in the model |
ANALYZE |
"analyze" |
Runs analysis, validation, or takeoff operations |
EXPORT |
"export" |
Exports data from the model |
SafetyMode
| Member | Value | Description |
|---|---|---|
READ_ONLY |
"read_only" |
Blocks all modify operations |
CAUTIOUS |
"cautious" |
Allows all operations but flags categories for confirmation |
FULL_ACCESS |
"full_access" |
Allows all operations (except explicitly blocked tools) |
ToolResultStatus
| Member | Value | Description |
|---|---|---|
SUCCESS |
"success" |
Tool executed successfully |
ERROR |
"error" |
Tool execution failed or was invalid |
DENIED |
"denied" |
Tool call was denied by the safety guard |
ParameterType
| Member | Value | Description |
|---|---|---|
STRING |
"string" |
String parameter |
INTEGER |
"integer" |
Integer parameter |
NUMBER |
"number" |
Floating-point number parameter |
BOOLEAN |
"boolean" |
Boolean parameter |
ARRAY |
"array" |
Array/list parameter |
OBJECT |
"object" |
Object/dict parameter |
McpMessageType
| Member | Value | Description |
|---|---|---|
REQUEST |
"request" |
Client-to-server JSON-RPC request |
RESPONSE |
"response" |
Server-to-client JSON-RPC response |
NOTIFICATION |
"notification" |
One-way notification (no response expected) |
Dataclass Reference
ToolParameter
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
– | Parameter name |
type |
ParameterType |
– | JSON Schema type |
description |
str |
– | Human-readable description |
required |
bool |
True |
Whether the parameter is required |
default |
Any |
None |
Default value when not supplied |
ToolDefinition
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
– | Tool name |
description |
str |
– | Human-readable tool description |
category |
ToolCategory |
– | Tool category |
parameters |
list[ToolParameter] |
[] |
Parameter definitions |
returns_description |
str |
"" |
Description of return value |
ToolResult
| Field | Type | Default | Description |
|---|---|---|---|
status |
ToolResultStatus |
– | Outcome status |
data |
Any |
None |
Result data on success |
error |
str or None |
None |
Error message on failure |
execution_time_ms |
float |
0.0 |
Execution time in milliseconds |
Full Example
A complete example that wires up all four components and runs the MCP server:
import asyncio
from revitpy.ai import (
McpServer,
McpServerConfig,
RevitTools,
SafetyGuard,
SafetyConfig,
SafetyMode,
PromptLibrary,
ToolCategory,
ToolDefinition,
ToolParameter,
ParameterType,
)
# 1. Set up tools
tools = RevitTools(context=revit_app)
tools.register_tool(
definition=ToolDefinition(
name="get_room_schedule",
description="Generate a room schedule from the model",
category=ToolCategory.ANALYZE,
parameters=[
ToolParameter(
name="level",
type=ParameterType.STRING,
description="Building level to filter by",
required=False,
default="all",
),
],
returns_description="Room schedule data",
),
handler=my_room_schedule_handler,
)
# 2. Configure safety
guard = SafetyGuard(config=SafetyConfig(
mode=SafetyMode.CAUTIOUS,
require_confirmation_for=[ToolCategory.MODIFY],
max_undo_stack=100,
))
# 3. Set up prompts
prompts = PromptLibrary()
prompts.register_template(
"room_analysis",
"Analyze the rooms on level :\n"
"",
)
# 4. Start the server
async def main():
async with McpServer(
tools,
config=McpServerConfig(host="localhost", port=8765),
safety_guard=guard,
prompt_library=prompts,
) as server:
print(f"MCP server running on {server.config.host}:{server.config.port}")
await asyncio.Event().wait() # Run until interrupted
asyncio.run(main())