Skip to main content
Glama

roofgeometry-prospect

A small MCP server and CLI that turns the Google Solar API roof data for one building into an installer-ready roof geometry packet: per-segment pitch (degrees and rise/12), azimuth, facing, area, plane height and sunshine quantiles, plus derived facts an installer or a sales agent asks first: how much usable south/west area there is, whether the roof is steep (fall protection, extra labour), how complex it is, and a shading proxy. Output is compact JSON, Markdown and a simple SVG roof sketch.

It can also ask for the Solar API's pre-GA roof geometry option (experiments=ROOF_GEOMETRY_INSIGHTS + additionalInsights=ROOF_GEOMETRY), which adds facet polygons and rooftop obstacles. That option is opt-in and labelled pre-GA everywhere; see What we checked.

The problem

buildingInsights:findClosest already returns roof segments with pitch, azimuth, area, height and sunshine, but as raw numbers an agent has to interpret: which faces are worth panels, which are too small, which are steep, how many planes a crew will deal with. Installers want that as a one-page answer before they roll a truck. This lab does the packaging in one tool call and labels the source of every threshold.

Related MCP server: RoofMeasure MCP

Quickstart

bun install --frozen-lockfile

# Keyless demo: synthetic buildings in the documented response shape (fictional addresses, open-ocean points)
bun run src/cli.ts roof --address "100 Example Ave, Sampletown" --fixtures
bun run src/cli.ts roof --address "100 Example Ave, Sampletown" --roof-geometry --svg roof.svg --fixtures
bun run src/cli.ts roof --address "9 Gable Row, Steepville" --format json --fixtures
bun run src/cli.ts roof --address "7 Sample Plaza, Calmtown" --min-quality MEDIUM --fixtures

# Live: put GOOGLE_MAPS_API_KEY in .env (see .env.example), then
bun run src/cli.ts roof --address "<address>" [--roof-geometry] [--steep-pitch 35] [--svg roof.svg]
bun run src/cli.ts roof --at=37.4,-122.1 --min-quality MEDIUM      # negative numbers need --at=...

# Methodology (formulas, thresholds, pre-GA status, references)
bun run src/cli.ts method

# MCP server (stdio)
bun run src/cli.ts serve            # live
bun run src/cli.ts serve --fixtures # keyless demo

MCP client config (Claude Code .mcp.json, Cursor mcp.json):

{
  "mcpServers": {
    "roofgeometry": {
      "command": "bun",
      "args": ["run", "/path/to/roofgeometry-prospect/src/cli.ts", "serve"],
      "env": { "GOOGLE_MAPS_API_KEY": "${GOOGLE_MAPS_API_KEY}", "ROOFGEO_BUDGET": "10" }
    }
  }
}

The key's Cloud project must have the Solar API (solar.googleapis.com) enabled with billing on, plus the Geocoding API for address lookups. Restrict the key to those APIs.

Tools

Tool

What it does

Google calls

roof_geometry

address or lat+lng; min_quality (HIGH/MEDIUM/LOW/BASE, default HIGH), exact_quality, roof_geometry (pre-GA, default false), fallback (default true), thresholds steep_pitch_deg, flat_pitch_deg, shade_threshold, min_panels; format json / markdown / svg / all (default json + markdown)

1 Geocoding v4 call for an address, plus 1 findClosest call (2 if a pre-GA request is rejected and retried)

geometry_method

Formulas, thresholds and their sources, what is verified about the pre-GA option, attribution, references

none

session_status

Budget used/remaining, refused calls

none

CLI: roofgeo roof | method | serve. Exit codes: 0 ok, 2 error, 64 usage.

What the packet contains

Per segment (copied from roofSegmentStats[i] unless marked derived):

Field

Meaning

pitchDeg, pitchRiseIn12

Pitch, and derived 12 x tan(pitch) (e.g. 6/12)

azimuthDeg, facing

Azimuth (dropped for flat segments: Google sets it to 0 there); derived 8-point facing or flat

areaM2, groundAreaM2

Roof area (tilt included) and footprint

heightMslM, heightAboveLowestSegmentM

Plane height at the segment center, meters above sea level; derived difference from the lowest segment

sunshine

min / p25 / median / p75 / max, interpolated from sunshineQuantiles (kWh/kW per year)

medianPctOfPeak, shadedShare

Derived: median vs maxSunshineHoursPerYear; share of area below 80% of that peak (shading proxy)

steep, prime, usable

Derived flags (rules below)

panelsInMaxLayout

Panels on this segment in the API's maximum layout (solarPanels[].segmentIndex)

polygon

Pre-GA only: rings, holes and ground footprint of polygonGeojson

Summary: primeAreaM2, usableAreaM2 + usableSegments, flatAreaM2, areaByFacing, steepRoof + steepSegments + steepAreaShare, complexity (score, tier), shading, obstacles (pre-GA), heightRangeM, maxArrayPanelsCount, panel size. Plus building id, imagery quality and date, distance from the query point, roofGeometry (requested / returned / pre-GA / fallback), thresholds with sources, notes, attribution.

Rules and their sources

Rule

Default

Source

steep = pitch > threshold

18.43° (slope > 4 in 12)

OSHA 29 CFR 1926.500(b) "steep roof"; override with steep_pitch_deg

steepRoof

any usable segment is steep (any segment if none is usable)

lab rule

flat = pitch < threshold

10°

lab default

prime = non-flat, facing within 90° of 225° (north of the equator: S, SW, W) or of 315° (south of it: N, NW, W)

lab rule

usable = (prime or flat) and area >= N x panel height x panel width

N = 4 panels, panel size from the response

lab default

shading proxy = share of area below X x maxSunshineHoursPerYear

X = 0.8

lab default

complexity score = segments + obstacles (obstacles only with pre-GA geometry)

tiers: simple <= 2, moderate <= 4, complex <= 8, else very-complex

lab default

far building note

center > 30 m from the query point

lab default

The SVG sketch draws the building and segment bounding boxes (dashed) or, with pre-GA geometry, the facet polygons (solid, holes cut out) and obstacles, on a local meter grid with north up, a scale bar, downslope arrows, hatching for steep faces and the attribution line. It is a schematic, not a map.

Sample sketch (synthetic building)

Measured

bun run report (keyless): the real MCP server through an MCP client, against synthetic fixtures (invented buildings, not real data). Byte counts are JSON/Markdown/SVG string lengths.

Scenario

Input

Calls

Imagery

Segments

Prime m²

Usable m²

Steep (threshold deg)

Max pitch

Complexity

Shaded share

Obstacles

Polygons

Fallback

Solar response bytes

Packet JSON bytes

Markdown bytes

SVG bytes

MCP = service

hip roof, GA fields

address

2

HIGH

6

90.6

124.6

yes (> 18.43, osha-4-in-12)

26.8

complex (6)

55%

-

false

false

8332

5763

2313

4409

true

hip roof, pre-GA roof geometry

address, roof_geometry

2

HIGH

6

90.6

124.6

yes (> 18.43, osha-4-in-12)

26.8

complex (8)

55%

2

true

false

9926

5969

2475

4642

true

hip roof, steep at 35 deg

address, steep_pitch_deg 35

2

HIGH

6

90.6

124.6

no (> 35, user)

26.8

complex (6)

55%

-

false

false

8332

5746

2230

4013

true

steep cut-up roof

address

2

HIGH

9

105.8

95.4

yes (> 18.43, osha-4-in-12)

50.0

very-complex (9)

71%

-

false

false

6498

7313

2628

6210

true

southern hemisphere gable

address

2

HIGH

3

76.8

76.8

yes (> 18.43, osha-4-in-12)

22.0

moderate (3)

59%

-

false

false

4135

4155

1990

3107

true

flat commercial, MEDIUM

address, min_quality MEDIUM

2

MEDIUM

2

0.0

480.1

no (> 18.43, osha-4-in-12)

1.1

simple (2)

7%

-

false

false

19220

3705

1893

2332

true

pre-GA rejected, GA fallback

lat/lng, roof_geometry

2

HIGH

2

46.2

46.2

yes (> 18.43, osha-4-in-12)

30.0

simple (2)

54%

-

false

true

2923

3794

2254

2750

true

  • Pre-GA polygons: ground footprint from polygonGeojson (shoelace) vs the segment's groundAreaMeters2: max difference 0% on the synthetic hip roof (a round-trip check of our projection and shoelace code, since the fixture is drawn with the same projection; not evidence about real Google polygons).

  • Flat-roof address at the default min_quality HIGH: google_NOT_FOUND (synthetic 404); with MEDIUM it answers (row above).

  • 403 SERVICE_DISABLED (synthetic copy of the error shape): google_PERMISSION_DENIED / SERVICE_DISABLED; with roof_geometry the same error, no fallback retry (SERVICE_DISABLED).

  • Pre-GA rejected with fallback: false: google_INVALID_ARGUMENT.

  • Point with no building: google_NOT_FOUND.

  • Budget 3, two address lookups (2 calls each): ok, budget_exceeded.

Live status (2026-10-21)

With a Google Maps Platform key from our test project (bun test tests/live, bun run record):

  • Geocoding v4: answered (the address resolves to the expected area).

  • Solar API findClosest, GA request and pre-GA roof geometry request: both HTTP 403 PERMISSION_DENIED, reason SERVICE_DISABLED (the Solar API is not enabled in that project, and we did not enable it). The live test prints a skipped: line for this case.

So no real Solar response has been seen by this lab, with or without roof geometry. The packet logic runs only on synthetic fixtures shaped from the docs.

What we checked in Google's docs

Checked on 2026-10-21.

Item

What we found

Source

HTML reference: experiments[]

Lists only EXPANDED_COVERAGE; requests using experiments are "classified as a pre-GA offering" under the Maps Service Specific Terms

findClosest, Experiment (both last updated 2026-05-12)

HTML reference: additionalInsights[]

Lists only DETECTED_ARRAYS

same

Discovery document

experiments enum includes ROOF_GEOMETRY_INSIGHTS ("Enables experimental roof geometry data via FindClosestBuildingInsights"; requires ROOF_GEOMETRY in additional_insights; error if passed to GetDataLayers). additionalInsights enum includes ROOF_GEOMETRY: adds polygonGeojson to each RoofSegmentSizeAndSunshineStats and obstacles to SolarPotential. New schema Obstacle { polygonGeojson }. Both GeoJSON fields: RFC 7946 Polygon, holes as extra loops

discovery doc, revision 20261019

When it appeared

The Wayback Machine copy of the discovery doc captured 2026-03-12 (revision 20260310) has neither value. The "Oct 2 2026" date in the idea could not be verified; we only know it is between those revisions

Wayback copy

Release notes

Newest entry May 11, 2026 (detected arrays GA); nothing about roof geometry

release notes (last updated 2026-10-20)

Pre-GA terms

Experimental: no SLA or support obligation, not intended for production, may change in backward-incompatible ways. Preview: no SLA, test environments

launch stages (last updated 2026-10-20)

GA roof segment fields used

pitchDegrees, azimuthDegrees (0 for flat), stats.areaMeters2 (tilt included), stats.groundAreaMeters2, stats.sunshineQuantiles ((N-1)-iles, kWh/kW), center, boundingBox, planeHeightAtCenterMeters (above sea level)

findClosest reference + discovery doc

Imagery quality

Discovery enum: HIGH, MEDIUM, LOW, BASE

discovery doc

Attribution

"Source: Includes solar data from Google" next to Solar data; "Includes data from Google Maps" for content created from it

Solar API policies (last updated 2026-10-20)

Caching

"Content pre-fetching, caching, or storage is generally restricted" (place IDs exempt)

same

Attribution and usage requirements

  • Attribution. Every JSON packet carries attribution ("Source: Includes solar data from Google") and derivedAttribution ("Includes data from Google Maps"); every Markdown packet and SVG sketch ends with both. Keep them next to the numbers wherever you show them.

  • No caching. This server keeps nothing between calls and the repo commits no Google responses. Store packets only as your agreement with Google allows.

  • Maps. If you show Solar results on a map, the policies require a Google Map; the SVG is a schematic without any basemap.

  • Pre-GA. roof_geometry: true uses an experimental option under Google's pre-GA terms. Treat it as test-only.

Limits

  • Unverified live. See Live status: every number in Measured comes from synthetic fixtures. Real responses may differ in fields, value formats or polygon semantics (for example, the third GeoJSON coordinate: the docs' example shows 0, its meaning is not documented; we ignore it).

  • Pre-GA option unverified. The roof geometry request and fields come only from the discovery document. We do not know whether ROOF_GEOMETRY works without the experiment flag, whether projects need allow-listing, or how it is billed. If Google rejects the request (400, or 403 other than API disabled/blocked), the tool retries once without it and says so.

  • Not a site survey or safety assessment. Roof data comes from aerial/satellite imagery. The steep flag uses the OSHA steep-roof slope as a planning signal; it says nothing about the safety of a roof. Labour cost is not estimated.

  • Heights are above sea level; there is no ground elevation in the response, so "height above ground" and storey count are not given. heightAboveLowestSegmentM is relative only.

  • Shading proxy comes from the sunshine quantiles, not a shade study; it cannot tell tree shade from self-shading or orientation.

  • Complexity tiers and other lab defaults are our choices, not an industry scale; they are labelled in every output and can be overridden where it matters (steep_pitch_deg, flat_pitch_deg, shade_threshold, min_panels).

  • Closest building. The API returns the building nearest the point; a bad geocode still gives a confident answer.

  • Budget scope. One upstream call budget per process / MCP connection (default 10), not per key or per day.

  • Keyless mode answers only the demo inputs in fixtures/synthetic/INDEX.json; anything else returns google_NO_FIXTURE.

  • Not included: the dataLayers endpoint (DSM / mask GeoTIFFs, which would give real 3D outlines), batch address lists, detected arrays (see solarscope-detected-arrays), financial analysis (see solarscope-mcp).

Development

bun run check               # install, skills layout, strict YAML, biome, tsc, unit + e2e tests, live tests (skip
                            # cleanly), report, bun audit, actionlint (if installed), gitleaks
bun test tests/unit tests/e2e
bun test tests/live         # needs GOOGLE_MAPS_API_KEY; prints "skipped: ..." otherwise (ROOFGEO_SKIP_LIVE=1 forces skip)
bun run fixtures:synthetic  # regenerate fixtures/synthetic from src/synthetic.ts
bun run record              # live: save real responses to gitignored .recorded-local/ for the shape check

Credits: builds on the SkilledDragonCorp minilabs solarscope-mcp and solarscope-detected-arrays (see NOTICE).

Not affiliated

Not affiliated with, endorsed by, or sponsored by Google LLC or OSHA. Google Maps Platform, Solar API and Geocoding API are trademarks of Google LLC.


Built by SkilledDragon LLC. Questions: hello@skilleddragon.com. Apache-2.0 (see LICENSE and NOTICE).

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables solar energy potential assessment by converting parcel centroid and acreage into annual/monthly generation estimates using the NREL PVWatts v8 API, suitable for revenue modeling and infographic headlines.
    -
  • A
    license
    A
    quality
    B
    maintenance
    Provides free EagleView-style satellite roof measurements and modular Xactimate-style estimating from Google Solar API data, enabling contractors to generate reports and estimates from any address.
    3
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Generates deterministic solar and energy storage system preliminary proposal calculations from structured customer inputs.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables rooftop solar installers to turn a CSV of tomorrow's booked jobs into a ranked crew schedule in Markdown and JSON, joining per-address geocoding, roof pitch/azimuth from the Google Solar API, hourly forecast checks against crew-safety thresholds, and estimated plane-of-array irradiance. Each job is returned as KEEP, MOVE (with a new same-day start) or SLIP, together with the specific rule that decided it.
    Apache 2.0