Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc1ba05ac1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Adds a new OpenStudio MCP demo under examples/openstudio_mcp_demo/, including an MCP server (model/sim/results tools), an AgentFactory-based A2A agent wired to that server, a Streamlit UI, policy/skill docs, a sample OpenStudio measure, and smoke tests.
Changes:
- Introduces an OpenStudio MCP server implementation (tools + runtime managers) with a basic OpenStudio simulation workflow and results querying.
- Adds a runnable demo package (agent bootstrap, Streamlit UI, launcher script, docs, policies, and a sample measure).
- Adds end-to-end smoke tests for tool discovery and key flows (model load, measure apply, sim run, results query).
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Updates locked version of automa-ai and adds missing wheel entries. |
| tests/test_mcp_openstudio_smoke.py | New smoke/e2e tests that start the MCP server and exercise tool flows. |
| examples/openstudio_mcp_demo/agent.py | AgentFactory + A2A server wiring to the OpenStudio MCP server and local skill/policy text. |
| examples/openstudio_mcp_demo/ui.py | Streamlit chat UI for the OpenStudio agent. |
| examples/openstudio_mcp_demo/run_all.sh | One-command launcher for agent + UI with log capture. |
| examples/openstudio_mcp_demo/openstudio_mcp_server/server.py | Core MCP server implementation and OpenStudio orchestration logic. |
| examples/openstudio_mcp_demo/openstudio_mcp_server/tools/* | Tool registration for model.*, sim.*, results.* plus shared schemas/error envelopes. |
| examples/openstudio_mcp_demo/openstudio_mcp_server/runtime/* | Workspace, job, artifact, and measure-registry helpers. |
| examples/openstudio_mcp_demo/measures/add_daylighting.py | Example OpenStudio Python measure invoked via model.apply_measure. |
| examples/openstudio_mcp_demo/policy/*.yaml | Demo policies for tool allowlist, run gates, and measure registry. |
| examples/openstudio_mcp_demo/skills/hvac_sizing_assistant.md | Skill contract describing the sizing workflow and allowed tools. |
| examples/openstudio_mcp_demo/README.md | Demo setup/run/troubleshooting documentation. |
| examples/openstudio_mcp_demo/ADVANCED_USER_GUIDE.md | Extension guide for measures/policies/skills. |
| examples/openstudio_mcp_demo/architecture_diagram.md | Mermaid diagrams for workflow/architecture. |
| examples/openstudio_mcp_demo/sample.env | Sample environment file for running the demo. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async for chunk in send_message_async(prompt, st.session_state.get("context_id")): | ||
| text_part = chunk.get("content") if isinstance(chunk, dict) else None | ||
| if text_part: | ||
| response_text += str(text_part) | ||
| placeholder.markdown(response_text + "▌") | ||
|
|
There was a problem hiding this comment.
This Streamlit UI assumes streaming chunks contain a top-level content field, but SimpleClient.send_streaming_message() yields A2A JSON-RPC-style objects (typically under result / delta / message, and containing context_id). As written, the UI will often render an empty response and never persist context_id for follow-up turns. Parse the chunk structure similarly to the existing examples/eplus_mcp_demo/ui.py and store context_id into st.session_state.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| def _resolve_model_path(self, model_uri: str) -> Path: | ||
| if model_uri.startswith("file://"): | ||
| parsed = urlparse(model_uri) | ||
| return Path(parsed.path).resolve() | ||
| return Path(model_uri).resolve() |
There was a problem hiding this comment.
_resolve_model_path() uses urlparse(model_uri).path for file:// URIs without URL-decoding. Path(...).as_uri() percent-encodes special characters (e.g., spaces -> %20), so valid local paths can incorrectly fail exists(). Decode the parsed path (e.g., via urllib.parse.unquote / url2pathname) before constructing the Path.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback. You should minimize the changes while addressing the issue. Do not add any new packages or introduce new patterns that may change the architecture of the software.
| MCP_HOST = "localhost" | ||
| MCP_PORT = 10210 | ||
| MCP_URL = f"http://{MCP_HOST}:{MCP_PORT}/sse" | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session", autouse=True) | ||
| def start_openstudio_mcp_server(): | ||
| from multiprocessing import Process | ||
| import time | ||
|
|
||
| process = Process(target=serve, args=(MCP_HOST, MCP_PORT, "sse"), daemon=True) | ||
| process.start() | ||
| time.sleep(2) | ||
| yield | ||
| process.terminate() | ||
|
|
There was a problem hiding this comment.
The test fixture starts the MCP server on a fixed port (10210) and waits via time.sleep(2), which can be flaky (slow startups) and can fail on CI/dev machines if the port is already in use. Consider selecting an available ephemeral port, waiting until the SSE endpoint is reachable, and ensuring cleanup via process.terminate(); process.join(timeout=...) (and possibly kill() as fallback).
This example shows an
AgentFactory-based AUTOMA-AI agent connected to a MCP server that exposes a minimal OpenStudio modeling/simulation lifecycle.What is
openstudio_mcp?openstudio_mcpis a real MCP server (Anthropicmcp/FastMCP) that exposes OpenStudio workflows as MCP tools for AUTOMA-AI agents.It provides:
model.*tools for model lifecycle operations.sim.*tools for asynchronous OpenStudio simulation execution.results.*tools for SQL-backed post-processing and summarization.Architecture
The example has four layers:
examples/openstudio_mcp_demo/agent.pyAgentFactory.examples/openstudio_mcp_demo/openstudio_mcp_server/server.pyruntime/workspace_manager.py: per-job sandbox folders and quota checks.runtime/job_manager.py:RUNNING/SUCCEEDED/FAILEDlifecycle.runtime/artifact_store.py: immutable artifact IDs and metadata.runtime/measure_registry.py: policy-based measure lookup and arg validation.policy/tool_allowlist.yamlpolicy/run_gates.yamlpolicy/measure_registry.yamlskills/hvac_sizing_assistant.mdModel tools
model.load(model_uri)model.clone(model_id)model.list_measures()model.set_weather(model_id, epw_id)model.set_design_days(model_id, ddy_id | derive_from_epw=true)(compatibility step)model.apply_measure(model_id, measure_id, args)model.validate(model_id)Simulation tools
sim.run(model_id, run_mode, options)returnsjob_idimmediately.sim.status(job_id)supports polling asynchronous simulation.sim.artifacts(job_id)returns result artifact IDs.Results tools
results.query(sql_id, query_type, params)supports:annual_end_use_fueldesign_day_end_use_fuelannual_euisizing_summarysql(advanced ad-hoc query)results.summarize(data, format)returns readable summary text/tables.Measures
examples/openstudio_mcp_demo/policy/measure_registry.yamladd_daylighting(examples/openstudio_mcp_demo/measures/add_daylighting.py)model.list_measures.model.apply_measureresolvesmeasure_idvia policy, validates args/defaults, executes with:openstudio execute_python_script <entrypoint>OSM_INPUT_PATH,OSM_OUTPUT_PATH,MEASURE_ARGS_JSONmodel_id.Measure interface contract
model.apply_measureis policy-driven:measure_idfrommeasure_registry.yaml.argsusing registered schema.openstudio execute_python_script <entrypoint>OSM_INPUT_PATHOSM_OUTPUT_PATHMEASURE_ARGS_JSONmodel_id.