Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# openstudio-mcp environment variable template.
# Copy to .env and fill in your values — .env is gitignored and must never be committed.

# ---------------------------------------------------------------------------
# Telemetry (optional — requires pip install 'openstudio-mcp[telemetry]')
# Leave TRACELOOP_BASE_URL unset to disable tracing entirely (zero overhead).
# ---------------------------------------------------------------------------

# OTLP HTTP endpoint. Examples:
# Local Jaeger: http://localhost:4318
# Traceloop cloud: https://api.traceloop.com
TRACELOOP_BASE_URL=

# API key — required only for Traceloop cloud, not for generic OTLP backends.
TRACELOOP_API_KEY=

# Service name shown on every span.
OTEL_SERVICE_NAME=openstudio-mcp

# Set to "false" to use synchronous span export (useful in development).
OTEL_EXPORT_BATCH=true

# IMPORTANT PRIVACY SETTING: when "true" (default), tool arguments and outputs
# — including file paths, model parameters, and simulation results — are
# exported to the OTLP backend. Set to "false" unless you have reviewed the
# data being exported and your backend is self-hosted or trusted.
TRACELOOP_TRACE_CONTENT=false
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ jobs:
mkdir -p runs
docker run --rm -v "$PWD:/repo" -v "$PWD/runs:/runs" openstudio-mcp:dev bash -lc 'cd /repo && pytest -vv -m "not integration"'

- name: Smoke-install telemetry extra
# Validates that traceloop-sdk and its deps install cleanly from the
# pinned constraint in [telemetry]. Catches packaging drift that would
# make openstudio-mcp[telemetry] uninstallable without needing a full
# Docker rebuild.
run: |
docker run --rm -v "$PWD:/repo" openstudio-mcp:dev bash -lc \
'pip install --quiet -e "/repo[telemetry]" && python -c "from traceloop.sdk import Traceloop; print(\"traceloop-sdk OK\")"'

- name: Save Docker image
run: docker save openstudio-mcp:dev | gzip > /tmp/image.tar.gz

Expand Down Expand Up @@ -81,8 +90,8 @@ jobs:
EXTRA_ENV=""
;;
5)
# HVAC supply sim smoke tests + hvac_validation + bar_building + concurrent regression
FILES="tests/test_hvac_supply_sim.py tests/test_hvac_validation.py tests/test_bar_building.py tests/test_concurrent_tools.py tests/test_stdout_logger_silence.py"
# HVAC supply sim smoke tests + hvac_validation + bar_building + concurrent regression + telemetry
FILES="tests/test_hvac_supply_sim.py tests/test_hvac_validation.py tests/test_bar_building.py tests/test_concurrent_tools.py tests/test_stdout_logger_silence.py tests/test_telemetry.py"
EXTRA_ENV=""
;;
esac
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,11 @@ Thumbs.db
# Code review artifacts
docs/review/

# Environment / secrets
.env
.env.*
!.env.example

# Codex CLI
.codex/
.mcp.json
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## [Unreleased]

### Added
- **Optional OpenLLMetry tracing**: `pip install 'openstudio-mcp[telemetry]'` + `TRACELOOP_BASE_URL` env var enables distributed tracing via traceloop-sdk. Zero overhead when unset. Key operations (`run_simulation`, `apply_measure`, `create_measure`, `create_*_building`, `run_qaqc_checks`) emit named spans; every FastMCP tool call is auto-instrumented via `McpInstrumentor`.
- **Docker tracing stack**: `docker/docker-compose.tracing.yml` + `docker/otel-collector-config.yaml` for local Jaeger/OTEL collector.
- **`test_telemetry.py`**: 20 unit tests for telemetry module (no Docker required) — includes startup-wiring and decorator-coverage regression tests.
- **`.env.example`**: template for telemetry environment variables with privacy guidance.

### Fixed
- **README tracing Docker example**: corrected image tag from `openstudio-mcp:dev` to `openstudio-mcp:tracing` (the dev image does not include traceloop-sdk); added build command and explanatory note.
- **`TRACELOOP_TRACE_CONTENT` docs**: expanded to warn that the default (`true`) exports tool arguments and outputs to the OTLP backend; recommends `false` as the safe starting point.
- **`opentelemetry-sdk` version constraint**: tightened `[dev]` lower bound from `>=1.20` to `>=1.38.0` to match `traceloop-sdk`'s actual minimum; prevents pip resolving an incompatible version when both extras are installed.

## [0.9.0] - 2026-04-10

### Added
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,40 @@ In **prod mode**, stdout is reserved exclusively for MCP JSON-RPC messages. Logs

---

## Tracing (OpenLLMetry)

Distributed tracing via [traceloop-sdk](https://github.com/traceloop/openllmetry) is available as an optional extra. Install it, then set `TRACELOOP_BASE_URL` to enable it:

```bash
pip install 'openstudio-mcp[telemetry]'
```

Or with Docker (requires the tracing image built with `--build-arg TELEMETRY=1`):

```bash
# Build the tracing-enabled image once:
docker build --build-arg TELEMETRY=1 -t openstudio-mcp:tracing -f docker/Dockerfile .

# Run with tracing enabled:
docker run --rm -i \
-e TRACELOOP_BASE_URL=http://host.docker.internal:4318 \
openstudio-mcp:tracing openstudio-mcp
```

The standard `openstudio-mcp:dev` image does **not** include `traceloop-sdk`. Using it with `TRACELOOP_BASE_URL` set will log a warning and disable tracing — it will not work silently.

| Variable | Default | Description |
|----------|---------|-------------|
| `TRACELOOP_BASE_URL` | *(unset — disabled)* | OTLP endpoint, e.g. `http://localhost:4318` or `https://api.traceloop.com` |
| `TRACELOOP_API_KEY` | *(unset)* | API key for Traceloop cloud (not needed for generic OTLP) |
| `OTEL_SERVICE_NAME` | `openstudio-mcp` | Service name on every span |
| `OTEL_EXPORT_BATCH` | `true` | Set `false` for synchronous export in development |
| `TRACELOOP_TRACE_CONTENT` | `true` | **Set `false` to protect privacy** — when `true`, tool arguments and outputs (including file paths and model data) are exported to the OTLP backend. Recommended: start with `false` and enable only if your backend is self-hosted or you have reviewed the data. |

Tracing is **off by default** and has zero overhead when `TRACELOOP_BASE_URL` is unset. Key operations (`run_simulation`, `apply_measure`, `create_measure`, the three `create_*_building` variants, and `run_qaqc_checks`) emit named spans. Every FastMCP tool call is auto-instrumented via `McpInstrumentor`.

---

## Architecture

- **Transport:** stdio (container spawned by host)
Expand Down
7 changes: 6 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ COPY pyproject.toml /repo/pyproject.toml
COPY mcp_server /repo/mcp_server
COPY docker /repo/docker

# TELEMETRY=1 installs traceloop-sdk + opentelemetry instrumentation for MCP.
# Default off to keep the image lean. Set to 1 for the tracing variant:
# docker build --build-arg TELEMETRY=1 -t openstudio-mcp:tracing -f docker/Dockerfile .
ARG TELEMETRY=0
RUN pip install --no-cache-dir -U pip \
&& pip install --no-cache-dir -e ".[dev]"
&& pip install --no-cache-dir -e ".[dev]" \
&& if [ "$TELEMETRY" = "1" ]; then pip install --no-cache-dir -e ".[telemetry]"; fi

# (Optional) If you want the container to include any other repo files too:
# COPY . /repo
Expand Down
91 changes: 91 additions & 0 deletions docker/docker-compose.tracing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# OpenLLMetry / Traceloop tracing stack for openstudio-mcp
#
# Quick start:
# # 1. Build the tracing-enabled MCP image (adds traceloop-sdk):
# docker build --build-arg TELEMETRY=1 -t openstudio-mcp:tracing -f docker/Dockerfile .
#
# # 2. Start Jaeger:
# docker compose -f docker/docker-compose.tracing.yml up -d
# open http://localhost:16686 # Jaeger UI — traces appear here
#
# # 3. Configure your MCP client to use the tracing image on the shared network.
# Add these flags to your client's docker run command:
#
# -e TRACELOOP_BASE_URL=http://jaeger:4318
# --network openstudio-mcp-tracing
# (and use openstudio-mcp:tracing instead of openstudio-mcp:dev)
#
# Example — Claude Code .mcp.json with tracing enabled:
#
# {
# "mcpServers": {
# "openstudio-mcp": {
# "command": "docker",
# "args": [
# "run", "--rm", "-i",
# "-v", "/abs/path/inputs:/inputs",
# "-v", "/abs/path/runs:/runs",
# "--network", "openstudio-mcp-tracing",
# "-e", "OPENSTUDIO_MCP_MODE=prod",
# "-e", "TRACELOOP_BASE_URL=http://jaeger:4318",
# "-e", "OTEL_SERVICE_NAME=openstudio-mcp",
# "-e", "TRACELOOP_TRACE_CONTENT=true",
# "openstudio-mcp:tracing", "openstudio-mcp"
# ]
# }
# }
# }
#
# Environment variables understood by the MCP server (see mcp_server/telemetry.py):
# TRACELOOP_BASE_URL OTLP HTTP endpoint (required to enable telemetry)
# TRACELOOP_API_KEY API key for Traceloop cloud (omit for local Jaeger)
# OTEL_SERVICE_NAME Service name on spans (default: openstudio-mcp)
# OTEL_EXPORT_BATCH "false" for sync export in dev (default: batch)
# TRACELOOP_TRACE_CONTENT "false" to omit tool args from spans (privacy)

services:
jaeger:
image: jaegertracing/jaeger:2.5.0
container_name: openstudio-mcp-jaeger
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver (used by traceloop-sdk)
environment:
- SPAN_STORAGE_TYPE=memory
networks:
- tracing
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:16686/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s

# Optional: OpenTelemetry Collector as a middle layer.
# Useful if you want to fan-out to multiple backends (Jaeger + Prometheus + Loki).
# Comment out and point TRACELOOP_BASE_URL directly to jaeger:4318 for simplest setup.
otel-collector:
image: otel/opentelemetry-collector-contrib:0.120.0
container_name: openstudio-mcp-otelcol
command: ["--config=/etc/otel/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel/config.yaml:ro
ports:
- "4319:4318" # OTLP HTTP (external port offset to avoid conflict with jaeger)
- "4320:4317" # OTLP gRPC (external port offset)
- "8888:8888" # Collector metrics (Prometheus scrape endpoint)
networks:
- tracing
depends_on:
jaeger:
condition: service_healthy
restart: unless-stopped
profiles:
- collector # only start with: docker compose --profile collector up

networks:
tracing:
name: openstudio-mcp-tracing
driver: bridge
59 changes: 59 additions & 0 deletions docker/otel-collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenTelemetry Collector config for openstudio-mcp tracing stack.
# Used only when running: docker compose --profile collector up
# For simple setups, skip this and point TRACELOOP_BASE_URL directly to jaeger:4318.

receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 256
spike_limit_mib: 64
# Add service name tag from resource attributes
resource:
attributes:
- key: service.namespace
value: openstudio-mcp
action: insert

exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true

# Uncomment to export to Traceloop cloud instead of / in addition to Jaeger:
# otlphttp/traceloop:
# endpoint: https://api.traceloop.com
# headers:
# Authorization: "Bearer ${TRACELOOP_API_KEY}"

# Prometheus metrics from the collector itself (scrape at :8888/metrics)
prometheus:
endpoint: 0.0.0.0:8888

# Debug: print spans to collector stdout (useful for development)
debug:
verbosity: basic
sampling_initial: 5
sampling_thereafter: 200

service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resource, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
Loading
Loading