Skip to main content
Glama
borovkov-d

BuildWindow

by borovkov-d

BuildWindow

MCP lab project: an agent plans construction work against a real weather forecast using two MCP servers.

This is a course assignment for the KSE AI Agentic School (an MCP integration assignment): build a custom MCP server for a real domain problem, then connect it — alongside an existing third-party MCP server — to an agent that uses both together to do something a single tool couldn't.

Overview

BuildWindow is an MCP (Model Context Protocol) lab project built around a concrete scheduling problem: given a list of construction works with dependencies between them, and a weather forecast for a city, produce a schedule that respects both. The agent that does this planning holds two separate MCP connections at once. The first is the external, Go-based OpenWeather MCP server (github.com/mschneider82/mcp-openweather), which the agent calls once per run to get live current conditions and a 5-day forecast for the requested city — this is the only place in the whole project where a network call happens. The second is this repository's own BuildWindow MCP server: a local, fully deterministic server with no network calls at runtime, backed by a local JSON dataset of construction work types and their weather limits, that exposes four tools encoding the construction-domain rules (weather-suitability verdicts, curing-time estimation, and multi-work scheduling).

The two servers deliberately do not overlap in responsibility. OpenWeather MCP is the only source of anything that changes day to day — the weather itself. BuildWindow MCP owns everything that is a fixed rule instead: what temperature, wind, humidity and precipitation a given work type tolerates, how long concrete takes to cure at a given temperature, and how to place several dependent works into the earliest non-prohibited windows across a multi-day forecast. The BuildWindow server is built with the official Python MCP SDK (package mcp, v2.0.0+), using its MCPServer class — note that this class was named FastMCP in older SDK versions and was renamed to MCPServer as of SDK v2.0.0. The agent that drives both connections is built with the Claude Agent SDK (claude-agent-sdk on PyPI).

The schedule-critical OpenWeather call is deliberately not made by the LLM. The upstream tool's actual output (confirmed by reading its source — see docs/tool-contracts.md) is a plain-text report, not JSON, and the only signal it gives for any failure (bad key, unrecognized city, unreachable provider) is a syntactically successful but empty response — there is no error text to react to. So agent/main.py calls it directly via a low-level MCP client, parses it with a small, unit-tested function (agent/normalize.py), and only then starts an LLM session — handing the model already-clean daily figures instead of asking it to interpret raw provider text. The LLM session is connected to both MCP servers (get_mcp_status() discovery shows both connections), and the model genuinely is allowed to call the weather tool itself (allowed_tools lists it explicitly) — but only for one current- conditions sentence in its final report, per the system prompt; the daily forecast that drives plan_work_schedule always comes from the deterministic pre-session fetch, never from the model's own call. Both servers are genuinely used in the agent's own flow, not just visible.

engineer input (city + work list)
  -> agent/main.py calls OpenWeather MCP directly (not via the LLM) for
     the schedule-critical daily forecast
  -> agent/normalize.py parses the plain-text response into daily figures
  -> (if no usable forecast: report plainly, stop -- no LLM session started)
  -> LLM session starts, connected to BOTH MCP servers; may itself call
     the weather tool once for current-conditions color commentary only
  -> given the daily forecast + works as plain JSON (the only input that
     ever drives scheduling)
  -> BuildWindow MCP  (plan_work_schedule, validate_work_window,
     estimate_curing_time, ...)
  -> schedule + explanation

Related MCP server: Weather MCP Server

Prerequisites

  • Python 3.12+ — this repo was built and tested against 3.12.3.

  • uv — used as the dependency manager for this project.

  • Go 1.24+ — only needed if you want to build the OpenWeather MCP server yourself (installed here via winget install --id GoLang.Go, currently Go 1.26.7). Not required to use the BuildWindow server or to run its tests.

  • An OpenWeather API key — only needed for a live agent run against real weather. Free tier available at openweathermap.org/api.

Installation

From the repo root:

uv sync

This creates a .venv and installs both the runtime dependencies (mcp, pydantic, claude-agent-sdk, python-dotenv) and the dev dependencies (pytest, ruff, black).

Configuration

Copy the example environment file and fill in your key:

Copy-Item .env.example .env

bash: cp .env.example .env

Then edit .env and set OWM_API_KEY to a real key from openweathermap.org/api (free tier). .env is gitignored — it is never committed.

agent/mcp_config.json is the single source of truth for both MCP server configurations. Its openweather entry references ${OWM_API_KEY} as a placeholder, which agent/main.py substitutes from the process environment at startup. Note that agent/main.py does not read .env itself — its main() calls python-dotenv's load_dotenv() first, and that call is what actually makes the values in .env reach the process environment before the substitution happens.

Its openweather.command field is itself a placeholder, ${MCP_OPENWEATHER_PATH}agent/main.py resolves it from the MCP_OPENWEATHER_PATH environment variable if set, or falls back to the bare command mcp-openweather (relying on PATH) if not. Set MCP_OPENWEATHER_PATH in .env (see .env.example) to the binary's absolute path if you don't want to add its directory to PATH — both were verified to work live.

Building the OpenWeather MCP server (only needed if you want a live run against real weather). These are the exact commands used to build and verify it in this repo's own development environment:

winget install --id GoLang.Go -e --accept-source-agreements --accept-package-agreements
# open a new shell so PATH picks up the Go toolchain, then:
go install github.com/mschneider82/mcp-openweather@main

This installs to $(go env GOPATH)\bin\mcp-openweather.exe — on Windows that's typically %USERPROFILE%\go\bin\mcp-openweather.exe. Important: the Go MSI installer adds the Go toolchain (C:\Program Files\Go\bin) to PATH, but does not add %USERPROFILE%\go\bin — where go install actually places built binaries. Either add that directory to PATH yourself, or set MCP_OPENWEATHER_PATH to the binary's full path (see above) — this repo's own setup uses the latter.

Why @main and not @latest: go install ...@latest resolves to tag v1.0.0, which is one real commit ("Fix #5") behind the repository's main branch. Both were built and compared live this project: v1.0.0 reads the optional units/lang arguments with no fallback when they're omitted entirely, so an omitted lang fails with language unavailable even though the tool's own schema declares a default; main's "Fix #5" commit adds defensive handling and the same call succeeds. The forecast template itself is otherwise identical between the two (confirmed by reading both versions' source) — building from main doesn't add per-day wind/humidity/precipitation, it only fixes the argument bug. agent/main.py always passes city, units="c", and lang="en" explicitly regardless, so this bug can't actually surface through this project either way — but main is the more robust binary to depend on if you ever call the tool a different way.

Do not use the -o mcp-weather flag shown in some of the upstream README's own examples — that produces a binary name inconsistent with its own config example. Build with the default name, mcp-openweather.

Running the MCP server

uv run python -m server.main

This runs the BuildWindow MCP server over stdio, independent of the agent process — it can be started and exercised entirely on its own. On success it prints exactly this line to stderr:

BuildWindow MCP server ready: 4 tools, 12 work types loaded

Running the agent

uv run python -m agent.main

With no arguments, this uses a built-in demo city ("Kyiv") and a built-in demo work list: excavation, then concrete_pour (depending on it), then concrete_finishing (depending on that).

Both can be overridden:

uv run python -m agent.main "CityName"
uv run python -m agent.main "CityName" '[{"work_code": "excavation", "duration_days": 1, "depends_on": []}]'

The optional second argument is a JSON array of works in the same shape as the demo list.

Replay mode--forecast-from-file <path> is fully offline: it replaces both the daily forecast and the current-conditions note with data read from a recorded file, via the same deterministic parsers (normalize_forecast, parse_current_conditions) used for a live call. openweather is not connected at all in this mode (confirmed via get_mcp_status() — only buildwindow appears), so a run needs no network access and no API key whatsoever — verified live with a deliberately broken OWM_API_KEY and an unreachable MCP_OPENWEATHER_PATH at the same time; the run still completed normally:

uv run python -m agent.main "Longyearbyen" '[{"work_code": "excavation", "duration_days": 1, "depends_on": []}, {"work_code": "exterior_painting", "duration_days": 2, "depends_on": []}]' --forecast-from-file fixtures/weather_longyearbyen.txt

fixtures/weather_kyiv.txt and fixtures/weather_longyearbyen.txt are real responses captured live this project (trimmed to 3 full real calendar days each, no key inside either) — not synthetic, not fabricated examples. Useful if the real weather in a demo city has changed by the time you're running this, or if there's no network at all at demo time.

A full live run against real weather requires a genuinely valid OWM_API_KEY (see Configuration above) — confirmed working in this repo's own development environment: uv run python -m agent.main "Kyiv" produces a real schedule from a real forecast, and uv run python -m agent.main "Longyearbyen" '[...]' demonstrates real weather actually forcing a reschedule (see docs/demo-checklist.md step 4). Without a working key, agent/main.py fetches the forecast directly (not via the LLM), gets an empty result, prints Forecast unavailable for '<city>' (...), and exits before starting any LLM session — no wasted model call, no fabricated schedule. This was verified with three real failure modes: the mcp-openweather binary unreachable at all, an invalid OWM_API_KEY, and an invalid city name — the last two are actually indistinguishable through this upstream tool (see docs/tool-contracts.md for why) and were both confirmed to fail the same clean way, even with a genuinely valid key active elsewhere in the same environment.

OpenWeather rate limits: one successful live run makes exactly two real calls to the weather tool (confirmed live, by counting) — the deterministic pre-session fetch, plus the model's own single current-conditions call (see Overview above). A failed live run (no usable forecast) makes exactly one, since the LLM session never starts. Replay mode (--forecast-from-file) makes zero — both the daily forecast and the current-conditions note come from the recorded file, and openweather isn't connected at all in this mode (confirmed live: get_mcp_status() shows only buildwindow). OpenWeather's free tier is documented at 60 calls/minute and 1,000,000 calls/month — comfortably enough for any number of manual demo runs; this project doesn't stress-test that published figure itself.

Project structure

.
├── README.md, DECISIONS.md, pyproject.toml, uv.lock, .env.example, .gitignore
├── docs/
│   ├── tool-contracts.md
│   ├── design-rationale.md
│   └── demo-checklist.md
├── scripts/
│   └── list_tools.py      # proves both MCP connections discover fine offline
├── server/
│   ├── main.py            # MCP server entry point, registers the 4 tools
│   ├── schemas.py         # Pydantic input/output models
│   ├── rules.py           # deterministic verdict/curing/planner logic
│   ├── dataset.py         # loads and validates work_types.json
│   ├── errors.py          # domain exceptions and error codes
│   └── data/work_types.json
├── agent/
│   ├── main.py             # agent entry point (Claude Agent SDK)
│   ├── normalize.py        # deterministic OpenWeather text -> daily figures
│   └── mcp_config.json     # config for both MCP servers
├── fixtures/
│   ├── weather_kyiv.txt      # real captured response, for --forecast-from-file
│   └── weather_longyearbyen.txt  # real captured response, for --forecast-from-file
└── tests/
    ├── conftest.py
    ├── test_dataset.py, test_lookup.py, test_validate.py
    ├── test_curing.py, test_planner.py, test_errors.py
    └── test_normalization.py

Tool overview

Tool

Summary

lookup_work_requirements

Look up a work type's, or an entire category's, weather limits.

validate_work_window

Check one work type against one day of weather and get back an itemized verdict.

estimate_curing_time

Estimate when a curing work type will actually be ready, given a sequence of daily temperatures.

plan_work_schedule

Place several dependent works across a multi-day forecast in one call.

The full contracts — exact JSON Schemas and real captured examples for every tool, including the external weather tool as used by this project — live in docs/tool-contracts.md.

Testing

uv run pytest -v
uv run ruff check .
uv run black --check .

All three currently pass cleanly in this repo: 51 tests pass (covering the 38 spec-required cases, a few supplementary assertions, and 8 tests for the agent/normalize.py weather-parsing module, including the two real-fixture and two current-conditions cases), and both ruff and black report no issues.

Limitations

The full reasoning behind each of these lives in docs/design-rationale.md — this list is intentionally brief:

  • Dataset thresholds are illustrative, not derived from real ДБН/ДСТУ standards.

  • The planner has no resource/crew constraints — works can overlap dates.

  • The real planning horizon is capped at 5 days by the OpenWeather provider.

  • Curing time uses a simplified Nurse-Saul maturity model.

  • One work occupies one continuous block — there is no split scheduling.

  • The OpenWeather MCP server's weather tool (confirmed by reading its source, not assumed) exposes only temperature per 3-hour forecast entry — wind speed and humidity are only available in a single current-conditions snapshot, applied here as a constant across every forecast day, and precipitation isn't exposed at all, so precipitation_mm is always 0.0 through this integration. This means BuildWindow's precipitation rule (a work with precipitation_allowed=false gets a hard violation if precipitation_mm > 0) can never actually trigger from a live run through this integration — it's real, correct code, covered by unit tests against constructed data (tests/test_validate.py, spec cases #16-17), but not something a live demo can show, since there is no live path to non-zero precipitation. This project does not simulate or inject fake rain data to manufacture that demo. The same upstream tool also can't distinguish a bad API key from an unrecognized city from an unreachable provider — all three come back as the same syntactically-successful-but-empty response, which is why agent/main.py can only report "no forecast available," not a specific cause, for any of those three cases. See docs/tool-contracts.md for the full, source-verified detail.

  • A genuinely valid OWM_API_KEY is now confirmed working: a full live run against real Kyiv weather produces a real schedule end-to-end, and a real cold-weather city (Longyearbyen) was found where the live forecast actually forces a work unschedulable and validate_work_window fires with real numbers — see docs/demo-checklist.md step 4. Everything described in this README has now been verified against a real, working key, not just an absent one; see DECISIONS.md for what that live run against real data did and didn't change in the code.

Documentation

  • docs/tool-contracts.md — the exact JSON Schema contracts for all four BuildWindow tools, and for the external OpenWeather weather tool as used by this project, each with a real captured example.

  • docs/design-rationale.md — why each tool exists, how the tool set maps onto the workflow, the boundaries between components, the trade-offs made, and the project's limitations in full.

  • docs/demo-checklist.md — a step-by-step checklist for running a live demo of the project.

  • DECISIONS.md — a dated log of implementation decisions, each with its rationale and the alternative that was rejected.

Install Server
F
license - not found
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    B
    quality
    D
    maintenance
    Enables users to get weather alerts and forecasts through natural language interactions. Provides real-time weather information and alert notifications via MCP tools.
    2
  • A
    license
    Not graded
    quality
    D
    maintenance
    Global weather intelligence for AI assistants providing 10 weather tools — forecasts, historical data, air quality, marine, geocoding, elevation, and climate projections at 1km resolution with 80+ years of archive.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Exposes Swiss weather forecast data as MCP tools, including rainfall, sunshine, temperature, wind, and more, with local caching.
    1
    Apache 2.0

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

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/borovkov-d/buildwindow-mcp'

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