Skip to content

add mcp demo#63

Open
weilixu wants to merge 3 commits intodevelopfrom
openstudio_mcp
Open

add mcp demo#63
weilixu wants to merge 3 commits intodevelopfrom
openstudio_mcp

Conversation

@weilixu
Copy link
Contributor

@weilixu weilixu commented Feb 28, 2026

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_mcp is a real MCP server (Anthropic mcp/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:

  1. Agent layer
  • examples/openstudio_mcp_demo/agent.py
  • Built with AgentFactory.
  • Connects to MCP and orchestrates workflow/tool calls.
  1. MCP server layer
  • examples/openstudio_mcp_demo/openstudio_mcp_server/server.py
  • Registers model/sim/results MCP tools.
  • Uses standard MCP success/error envelope.
  1. Runtime/state layer
  • runtime/workspace_manager.py: per-job sandbox folders and quota checks.
  • runtime/job_manager.py: RUNNING/SUCCEEDED/FAILED lifecycle.
  • runtime/artifact_store.py: immutable artifact IDs and metadata.
  • runtime/measure_registry.py: policy-based measure lookup and arg validation.
  1. Governance/extension layer
  • policy/tool_allowlist.yaml
  • policy/run_gates.yaml
  • policy/measure_registry.yaml
  • skills/hvac_sizing_assistant.md

Model 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) returns job_id immediately.
  • 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_fuel
  • design_day_end_use_fuel
  • annual_eui
  • sizing_summary
  • sql (advanced ad-hoc query)

results.summarize(data, format) returns readable summary text/tables.

Measures

  • Measure registry policy: examples/openstudio_mcp_demo/policy/measure_registry.yaml
  • Built-in measure example: add_daylighting (examples/openstudio_mcp_demo/measures/add_daylighting.py)
  • Discover measures at runtime with model.list_measures.
  • model.apply_measure resolves measure_id via policy, validates args/defaults, executes with:
    • openstudio execute_python_script <entrypoint>
    • environment variables OSM_INPUT_PATH, OSM_OUTPUT_PATH, MEASURE_ARGS_JSON
  • On success, a new model artifact/state is created and returned as model_id.

Measure interface contract

model.apply_measure is policy-driven:

  1. Resolve measure_id from measure_registry.yaml.
  2. Validate/default args using registered schema.
  3. Execute script using:
    • openstudio execute_python_script <entrypoint>
  4. Pass runtime env vars:
    • OSM_INPUT_PATH
    • OSM_OUTPUT_PATH
    • MEASURE_ARGS_JSON
  5. Register a new immutable model artifact and return its model_id.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +50 to +55
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 + "▌")

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +399 to +403
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()
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +22 to +37
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()

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI commented Mar 2, 2026

@weilixu I've opened a new pull request, #64, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Mar 2, 2026

@weilixu I've opened a new pull request, #65, to work on those changes. Once the pull request is ready, I'll request review from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants