Skip to main content
Glama
DEVELOPER_NOTES.md13 kB
# OpenStudio MCP Server - Developer Notes ## How the Server Works ### Architecture Overview ``` User (in Claude Desktop) ↓ Claude AI Assistant ↓ MCP Protocol (stdin/stdout JSON-RPC) ↓ FastMCP Server (server.py) ↓ OpenStudioManager (openstudio_manager.py) ↓ OpenStudio Toolkit (openstudio_toolkit library) ↓ OpenStudio SDK (C++ library with Python bindings) ``` ### Component Responsibilities 1. **server.py** - MCP tool definitions - Defines all MCP tools with `@mcp.tool()` decorator - Handles parameter validation - Converts results to JSON strings - Returns errors in consistent format 2. **openstudio_manager.py** - Business logic layer - Maintains state (current_model, current_file_path) - Wraps OpenStudio Toolkit functions - Converts Python objects to JSON-serializable dicts - Provides error handling and logging 3. **openstudio_toolkit** - OpenStudio operations - Your library with domain-specific functions - Uses OpenStudio SDK directly - Returns pandas DataFrames and OpenStudio objects 4. **config.py** - Configuration management - Auto-detects OpenStudio installation - Manages file paths (workspace, sample_files, outputs) - Sets up logging ## Typical User Flow ### Scenario 1: User Loads and Analyzes a Model **User message**: "Load R2F-Office-Hub-006.osm and tell me about the spaces" **Claude's internal process**: 1. **Recognizes task**: Need to load model and get space information 2. **Tool selection**: - `load_osm_model` (to load the file) - `list_spaces` (to get space data) 3. **Execution sequence**: **Step 1: load_osm_model** ``` Claude calls: load_osm_model("R2F-Office-Hub-006.osm") server.py receives: { "tool": "load_osm_model", "arguments": { "file_path": "R2F-Office-Hub-006.osm", "translate_version": true } } server.py:load_osm_model() executes: - Logs the call - Calls os_manager.load_osm_file("R2F-Office-Hub-006.osm", True) openstudio_manager.py:load_osm_file() executes: - Calls resolve_osm_path(config, "R2F-Office-Hub-006.osm") → Searches workspace, sample_files, models directories → Finds: /workspace/sample_files/models/R2F-Office-Hub-006.osm - Checks if file exists (os.path.exists) → Returns True - Calls OpenStudio Toolkit: from openstudio_toolkit.utils.osm_utils import load_osm_file_as_model model = load_osm_file_as_model(resolved_path, translate_version=True) - OpenStudio Toolkit: → Uses openstudio.model.Model.load(file_path) → Optionally translates to current version → Returns OpenStudio Model object - Updates state: self.current_model = model self.current_file_path = resolved_path - Gets building info: building_info = self._get_building_summary() → Calls openstudio_toolkit.osm_objects.building.get_building_object_as_dataframe → Returns DataFrame with building properties → Converts to dict - Returns success dict: { "status": "success", "message": "Successfully loaded OSM file: R2F-Office-Hub-006.osm", "file_path": "/workspace/sample_files/models/R2F-Office-Hub-006.osm", "model_info": { "name": "R2F-Office-Hub-006", "floor_area": 1234.5, ... } } server.py converts to JSON string and returns to Claude ``` **Step 2: list_spaces** ``` Claude calls: list_spaces() server.py receives: { "tool": "list_spaces", "arguments": {} } server.py:list_spaces() executes: - Calls os_manager.get_all_spaces() openstudio_manager.py:get_all_spaces() executes: - Checks: self._check_model_loaded() → Raises ValueError if self.current_model is None - Calls OpenStudio Toolkit: from openstudio_toolkit.osm_objects.spaces import get_all_space_objects_as_dataframe df = get_all_space_objects_as_dataframe(self.current_model) - OpenStudio Toolkit: → Iterates through model.getSpaces() → Extracts properties: name, floor_area, thermal_zone, etc. → Returns pandas DataFrame - Converts DataFrame to list of dicts: spaces = df.to_dict(orient='records') - Returns: { "status": "success", "count": 12, "spaces": [ {"name": "Space 1", "floor_area": 100, ...}, {"name": "Space 2", "floor_area": 150, ...}, ... ] } server.py converts to JSON string and returns ``` 4. **Claude interprets results**: - Reads the JSON responses - Extracts key information (12 spaces, names, areas) - Composes natural language response 5. **Claude responds to user**: "I've loaded the model R2F-Office-Hub-006.osm. It contains 12 spaces with a total floor area of 1,234 m². The spaces include..." ### Scenario 2: User Converts to IDF **User message**: "Convert this model to EnergyPlus format" **Claude's process**: 1. **Tool selection**: `convert_to_idf` 2. **Execution**: ``` Claude calls: convert_to_idf() server.py:convert_to_idf() executes: - Calls os_manager.convert_to_idf(output_path=None) openstudio_manager.py:convert_to_idf() executes: - Checks model loaded - Generates output path: If output_path is None: base_name = "R2F-Office-Hub-006" # from current_file_path output_path = "/workspace/outputs/R2F-Office-Hub-006.idf" - Calls OpenStudio Toolkit: from openstudio_toolkit.utils.osm_utils import convert_osm_to_idf convert_osm_to_idf(self.current_model, output_path) - OpenStudio Toolkit: → Uses OpenStudio ForwardTranslator → Converts OSM objects to IDF format → Writes IDF file - Returns: { "status": "success", "message": "Successfully converted to IDF: R2F-Office-Hub-006.idf", "file_path": "/workspace/outputs/R2F-Office-Hub-006.idf" } ``` 3. **Claude responds**: "I've converted the model to EnergyPlus IDF format. The file is saved at outputs/R2F-Office-Hub-006.idf" ## What Each Tool Returns ### File Operations **load_osm_model** ```json { "status": "success", "message": "Successfully loaded OSM file: model.osm", "file_path": "/workspace/sample_files/models/model.osm", "model_info": { "name": "Building Name", "floor_area": 1234.5, "number_of_stories": 3 } } ``` **save_osm_model** ```json { "status": "success", "message": "Successfully saved OSM file: model.osm", "file_path": "/workspace/outputs/model.osm" } ``` **convert_to_idf** ```json { "status": "success", "message": "Successfully converted to IDF: model.idf", "file_path": "/workspace/outputs/model.idf" } ``` **copy_file** ```json { "status": "success", "message": "Successfully copied file", "source": { "original_path": "source.osm", "resolved_path": "/workspace/sample_files/models/source.osm", "size_bytes": 12345 }, "target": { "original_path": "target.osm", "resolved_path": "/workspace/outputs/target.osm", "size_bytes": 12345 }, "copy_duration_seconds": 0.05 } ``` ### Information Retrieval **get_model_summary** ```json { "status": "success", "model_loaded": true, "file_path": "/workspace/sample_files/models/model.osm", "building": { "name": "Building Name", "floor_area": 1234.5 }, "counts": { "spaces": 12, "thermal_zones": 4, "surfaces": 68 } } ``` **list_spaces** ```json { "status": "success", "count": 12, "spaces": [ { "name": "Space 1", "floor_area": 100.5, "volume": 350.0, "thermal_zone": "Zone 1" }, ... ] } ``` **list_thermal_zones** ```json { "status": "success", "count": 4, "thermal_zones": [ { "name": "Zone 1", "multiplier": 1, "spaces_count": 3 }, ... ] } ``` **list_materials** ```json { "status": "success", "count": 25, "materials": [ { "name": "Concrete", "thickness": 0.2, "conductivity": 1.8, "density": 2400, "specific_heat": 840 }, ... ] } ``` ### Error Responses All tools return errors in this format: ```json { "status": "error", "error": "Detailed error message here" } ``` Common errors: - "No model loaded. Load a model first." - "OSM file not found: /path/to/file.osm" - "Failed to load OSM file: Invalid format" ## Bugs That Were Fixed ### Bug 1: Wrong workspace_root in Docker **Problem**: Config had wrong workspace_root ```python workspace_root: str = "/workspace/openstudio-mcp-server" # Wrong! ``` **But Docker mounts to**: `-v C:\openstudio-mcp-server:/workspace` **Fix**: Changed config.py: ```python workspace_root: str = "/workspace" # Correct! ``` ### Bug 2: Claude Desktop Can't Find Uploaded Files **Problem**: Server runs directly in Claude Desktop (not Docker), where: - Files uploaded to `/mnt/user-data/uploads/` - Server only searched `/workspace/` (which doesn't exist there) - Environment is actually `/home/claude/` **Fix**: Added Claude Desktop paths to search strategy in `path_utils.py`: ```python # Search in Claude Desktop uploads directory if os.path.exists("/mnt/user-data/uploads"): search_paths.append(("Claude uploads", "/mnt/user-data/uploads/...")) # Search in Claude Desktop home directory if os.path.exists("/home/claude"): search_paths.append(("Claude home", "/home/claude/...")) ``` **Result**: Server now works in **both environments**: - Docker: Uses `/workspace/` paths ✓ - Claude Desktop: Uses `/mnt/user-data/uploads/` and `/home/claude/` ✓ ## How Claude Should Use the Tools ### Pattern 1: Load and Analyze ``` User: "Analyze building.osm" Claude should: 1. load_osm_model("building.osm") 2. get_model_summary() 3. list_spaces() 4. list_thermal_zones() 5. Summarize findings in natural language ``` ### Pattern 2: Specific Query ``` User: "What materials are used?" Claude should: 1. Check if model is loaded (may need to load first) 2. list_materials() 3. Present the materials list ``` ### Pattern 3: Export ``` User: "Export to EnergyPlus" Claude should: 1. Check if model is loaded 2. convert_to_idf() 3. Confirm the output file location ``` ### Pattern 4: Compare Models ``` User: "Compare model1.osm and model2.osm" Claude should: 1. load_osm_model("model1.osm") 2. get_model_summary() → save results 3. list_spaces() → save results 4. load_osm_model("model2.osm") 5. get_model_summary() → compare with saved 6. list_spaces() → compare with saved 7. Present comparison ``` ## Common Issues and Solutions ### Issue: "No module named 'openstudio_toolkit'" **Cause**: Toolkit not installed or not in Python path **Solution**: - Manual install: Copy `openstudio_toolkit/` to project root - Or: Install from git in pyproject.toml (requires git in Docker) ### Issue: "Model not loaded" error **Cause**: Claude tries to use inspection tools without loading model first **Solution**: Claude should always load model first or check `get_current_model_status` ### Issue: "File not found" even though file exists **Cause**: 1. Wrong workspace_root (fixed now) 2. File not in mounted volume 3. Permission issues **Solution**: Check Docker mount, verify paths ### Issue: Tools return empty results **Cause**: Model is valid but has no objects of that type **Example**: `list_air_loops()` returns `{"count": 0, "air_loops": []}` if model has no HVAC systems **Solution**: This is correct behavior, Claude should handle gracefully ## Testing the Server ### Quick Test ```bash docker run --rm -i \ -v "C:\openstudio-mcp-server:/workspace" \ openstudio-mcp-dev bash -c " cd /workspace && uv run python -c ' from openstudio_mcp_server import OpenStudioManager, get_config import os config = get_config() manager = OpenStudioManager(config) # Test file exists test_file = os.path.join(config.paths.sample_files_path, \"models\", \"R2F-Office-Hub-006.osm\") print(f\"File exists: {os.path.exists(test_file)}\") # Test load result = manager.load_osm_file(test_file) print(f\"Load status: {result[\"status\"]}\") # Test get spaces spaces = manager.get_all_spaces() print(f\"Spaces count: {spaces[\"count\"]}\") ' " ``` Expected output: ``` File exists: True Load status: success Spaces count: 12 ``` ## Next Steps for Development ### Priority Fixes 1. ✓ Fix workspace_root path (DONE) 2. Test all tools with the actual model file 3. Add error handling for missing OpenStudio dependencies 4. Add validation for tool parameters ### Future Enhancements 1. Add more modification tools (create spaces, zones, etc.) 2. Add simulation execution tools 3. Add results parsing tools 4. Add geometry creation tools 5. Add schedule creation/modification tools ### Testing Recommendations 1. Create unit tests for each tool 2. Test with various model sizes 3. Test error conditions 4. Test with invalid inputs 5. Performance testing with large models --- **Current Status**: Server should now work correctly. The workspace_root fix resolves the "file not found" issue.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/roruizf/openstudio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server