EnergyPlus MCP: epJSON Edition

December, 2025

In my last blog post, I wrote about a recent webinar I attended that showcased LBNL's EnergyPlus MCP server. I was inspired by the server's capabilities for manipulating EnergyPlus input files and automating workflows. I immediately began playing around with it and testing the tools. Most of the tools rely on eppy for parsing, manipulating, and writing EnergyPlus IDF files. Having recently developed similar tools that leverage the epJSON input file format, I realized that the tools could be refactored to eliminate their dependency on eppy.

So, I forked the repository, called it EnergyPlus-MCP-epJSON, and got to work re-writing all the tools without relying on eppy. You might be wondering: why does the file format matter? After all, both IDF and epJSON represent the same EnergyPlus model data. The answer lies in how we interact with that data programmatically.

While eppy is a solid tool, it adds an external dependency with its own overhead and complexity. The epJSON fork uses only Python's standard library - specifically the built-in JSON module. No external parsing libraries, no additional dependencies to install or maintain; just native Python dictionaries and lists representing the EnergyPlus model. A similar fork could be made for just about any scripting language, since they almost all have built-in JSON modules, including Ruby, Javascript, and PHP. I chose to stick with Python because that's what the rest of the library is written in and it's also the most popular scripting language.

Eliminating the eppy Dependency

The shift from using eppy to using on only the Python standard library has significant benefits:

  1. Performance: No intermediate parsing layer. When you load an epJSON file, Python's highly optimized C-based json parser reads it directly into memory as a dictionary. With IDF files, eppy must first parse the text format, then construct Python objects. The epJSON approach skips that entire overhead.
  2. Reliability: Standard library code is battle-tested, stable, and maintained by the core Python team. No risk of dependency conflicts, no waiting for third-party updates, no compatibility issues between versions.
  3. Transparency: JSON dictionaries are native Python data structures. If an issue arises, you only need to investigate your own code. If you encounter a problem while using eppy, the error might stem from either your code or eppy's internal logic.
  4. Elegance: Scripts that leverage Python dictionaries are more elegant and performant than the equivalent eppy functions. Let me show you a concrete example, comparing the _get_branch_details method before and after:

_get_branch_details using eppy

# OLD: IDF with eppy - Requires external library + brittle numbered fields
def _get_branch_details(self, idf, branch_name: str) -> Optional[Dict[str, Any]]:
    """Helper method to get detailed information about a specific branch"""
    branch_objs = idf.idfobjects.get("Branch", [])
    
    for branch in branch_objs:
        if getattr(branch, 'Name', '') == branch_name:
            branch_info = {
                "name": branch_name,
                "components": []
            }
            
            # Get all components in the branch
            for i in range(1, 20):  # EnergyPlus branches can have multiple components - hope 20 is enough!
                comp_type_field = f"Component_{i}_Object_Type" if i > 1 else "Component_1_Object_Type"
                comp_name_field = f"Component_{i}_Name" if i > 1 else "Component_1_Name"
                comp_inlet_field = f"Component_{i}_Inlet_Node_Name" if i > 1 else "Component_1_Inlet_Node_Name"
                comp_outlet_field = f"Component_{i}_Outlet_Node_Name" if i > 1 else "Component_1_Outlet_Node_Name"
                
                comp_type = getattr(branch, comp_type_field, None)
                comp_name = getattr(branch, comp_name_field, None)
                
                if not comp_type or not comp_name:
                    break
                
                component_info = {
                    "type": comp_type,
                    "name": comp_name,
                    "inlet_node": getattr(branch, comp_inlet_field, 'Unknown'),
                    "outlet_node": getattr(branch, comp_outlet_field, 'Unknown')
                }
                branch_info["components"].append(component_info)
            
            return branch_info
    
    return None

Notice: external import, special eppy objects, hardcoded limits, conditional field naming. This is 32 lines fighting against both the format AND the library.

_get_branch_details using the epJSON dictionary-based approach

Here's the exact same method in the epJSON fork using only the Python standard library:

# NEW: epJSON - Standard library only, clean and Pythonic
def _get_branch_details(self, ep, branch_name: str) -> Optional[Dict[str, Any]]:
    """Helper method to get detailed information about a specific branch"""
    branch_objs = ep.get("Branch", {})
    if branch_name in branch_objs:
        branch_info = {
            "name": branch_name,
            "components": []
        }
        comps = branch_objs[branch_name]["components"]
        for comp in comps:
            component_info = {
                "type": comp["component_object_type"],
                "name": comp["component_name"],
                "inlet_node": comp["component_inlet_node_name"],
                "outlet_node": comp["component_outlet_node_name"]
            }
            branch_info["components"].append(component_info)
        return branch_info       
    return None

Eighteen lines total. No external dependencies. No magic numbers. No conditionals. Just natural Python iteration over a list. The epJSON format stores components as actual arrays, and Python's json module gives us instant access.

Performance Benefit 1: Direct JSON Parsing

Let's talk about file loading performance.

The IDF approach:

from eppy.modeleditor import IDF
idf = IDF(idf_path)  # Parse text → Build eppy objects → Attribute wrappers

This involves multiple steps: reading the text file, parsing IDF syntax, creating Python objects for each element, wrapping them in eppy's attribute system.

The epJSON approach:

import json
with open(epjson_path) as f:
    ep = json.load(f)  # JSON → Python dict (single optimized C operation)

Python's json module is implemented in C for speed. It reads the file and creates native Python dictionaries in one highly optimized operation. No intermediate object creation, no attribute wrapper overhead.

For large models with thousands of objects, this difference is measurable. Loading times can be 2-3x faster with epJSON.

Performance Benefit 2: O(1) vs O(n) Lookups

Beyond file loading, the runtime performance is dramatically better.

The IDF/eppy approach requires linear search:

branch_objs = idf.idfobjects.get("Branch", [])  # Returns a list
for branch in branch_objs:
    if getattr(branch, 'Name', '') == branch_name:
        # Found it!

The epJSON approach uses dictionary keys:

branch_objs = ep.get("Branch", {})  # Returns a dict
if branch_name in branch_objs:  # O(1) lookup
    # Found it instantly!

For models with hundreds of branches, zones, or surfaces, this means dramatically faster operations. No external library overhead, just Python's highly optimized dictionary implementation.

Performance Benefit 3: Native Data Structures

The epJSON format uses Python's native data structures throughout - no wrapper classes, no attribute lookup overhead. This means:

  • Lists for collections: HVAC branches, outlet branches, inlet branches - all stored as Python lists you can iterate normally
  • Dictionaries for objects: Direct key access instead of getattr calls through eppy's attribute system
  • Consistent naming: Everything uses snake_case, matching Python conventions
  • Memory efficiency: No duplicate data in wrapper objects, just the raw dictionary

This isn't just cleaner code - it's faster code with a smaller memory footprint. Every operation uses Python's optimized built-in implementations.

Other Benefits

  • Environment Compatibility: Works anywhere Python works. No concerns about eppy compatibility with specific Python versions or operating systems.
  • Security: Smaller dependency tree means smaller attack surface. Every external library is a potential security risk - the epJSON fork minimizes this.
  • Maintenance: No reliance on eppy to release compatible versions or bug fixes.

More Capabilities!

I wanted to make the epJSON fork even more compelling for users and developers. The first step was to eliminate the eppy dependency to create tools that are faster and more maintainable. Secondly, I wanted to enable backwards compatibility with IDF, even though the operations leverage epJSON. This is accomplished by automatically converting IDFs to epJSON format if the user selects an IDF input file. Converting between IDF and epJSON is computationally trivial, so this wasn't a big lift.

Lastly, I added a new tool, which was requested during the webinar:

  • set_exterior_wall_construction: This method applies exterior wall constructions that are minimally complaint with either ASHRAE 90.1 or IECC (all code vintages up to 2022).
Let's see this new feature in action:

set_exterior_wall_constructions
set_exterior_wall_constructions tool.

Based on a single-sentence prompt, the LLM:

  • Ran list_available_files to find a Small Office model in the ExampleFiles folder
  • Copied ASHRAE901_OfficeSmall_STD2019_Denver.idf to outputs (using copy_file)
  • Converted the IDF to epJSON format (using convert_idf_to_epjson)
  • Identified all the exterior walls in the mode (using find_exterior_walls)
  • Determined which climate zone the model is located in (5B, Denver)
  • Created the mass wall construction meeting minimum compliance with 90.1-2022 in climate zone 5B and applied that construction to all exterior walls (set_exterior_wall_construction)

The u-factor of the new exterior wall construction was confirmed with a follow up prompt (What is the u-factor (in IP units) of the exterior walls?).

I plan to add many more tools that are helpful in my daily workflows, and I encourage others to check out EnergyPlus-MCP-epjson (or the original EnergyPlus MCP server), and contribute theirs too!