Skip to main content
Glama

solarscope-mcp

A small MCP server and CLI that answers "is this roof worth solar?" for an address. It geocodes the address, asks the Google Solar API (buildingInsights:findClosest) what fits on the closest building's roof, and turns that into a transparent proposal: a few system sizes with yearly energy, cost, incentives, year-1 savings and payback computed from your tariff, install cost per watt and incentives. When the API returns its own financialAnalyses, they are shown side by side with yours, never mixed in.

The problem

Solar installers and homeowners want a fast, credible first answer for one roof: how big a system fits, how much it produces, and whether it pays back at local prices. The Solar API has the roof data (panel layouts, yearly DC energy per layout, roof segments) and, in some areas, its own financial analysis, but an agent still has to geocode, pick layouts, convert DC to AC, apply prices and keep track of which numbers are assumptions. This lab does that in one tool call and labels the source of every input.

Related MCP server: Solar Sathi MCP Server

Quickstart

bun install --frozen-lockfile

# Keyless demo: synthetic building in the documented response shape (fictional address, invented values)
bun run src/cli.ts roof --address "100 Example Ave, Sampletown" --example-inputs --fixtures
bun run src/cli.ts roof --address "100 Example Ave, Sampletown" --tariff 0.25 --cost-per-watt 2.8 \
  --incentive-pct 10 --monthly-bill 120 --format json --fixtures
bun run src/cli.ts roof --address "7 Sample Plaza, Calmtown" --min-quality MEDIUM --example-inputs --fixtures

# Live: put GOOGLE_MAPS_API_KEY in .env (see .env.example), then
bun run src/cli.ts roof --address "<address>" --tariff <your price per kWh> --cost-per-watt <installer quote per W> \
  [--incentive-pct <percent>] [--incentive-amount <rebate>] [--annual-kwh <usage> | --monthly-bill <bill>]
bun run src/cli.ts roof --at=37.4,-122.1 --min-quality MEDIUM     # negative numbers need --at=...

# Methodology (formulas, constants, example inputs, 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": {
    "solarscope": {
      "command": "bun",
      "args": ["run", "/path/to/solarscope-mcp/src/cli.ts", "serve"],
      "env": { "GOOGLE_MAPS_API_KEY": "${GOOGLE_MAPS_API_KEY}", "SOLARSCOPE_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_potential

address or lat+lng; prices tariff_per_kwh, install_cost_per_watt, incentive_percent, incentive_amount, export_rate_per_kwh; usage annual_kwh or monthly_bill; lifetime_years, currency, panel_counts[], use_example_inputs, min_quality (HIGH/MEDIUM/LOW, default HIGH), exact_quality, format (json/markdown/both)

1 Geocoding v4 call for an address, plus 1 findClosest call

proposal_method

Formulas, input sources, Google's US method constants, the example inputs, verdict rule, attribution, reference URLs

none

session_status

Budget used/remaining, refused calls

none

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

How the proposal is computed

  1. Layouts. From solarPotential.solarPanelConfigs (ordered by panel count; config N uses the first N panels): 25%, 50% and 75% of the largest layout, the largest, the largest layout whose first-year AC output stays within your yearly usage (if given), the layout the API's own analysis picked for its default bill (if any), and any panel_counts you ask for.

  2. Size and energy. kW = panelsCount x panelCapacityWatts / 1000. Year-1 AC kWh = yearlyEnergyDcKwh x 0.85. Output declines by factor 0.995 per year.

  3. Cost. watts x install_cost_per_watt; incentives = cost x incentive_percent / 100 + incentive_amount, capped at the cost.

  4. Savings. Year n = (self-consumed kWh x tariff + exported kWh x export rate) x 1.022^(n-1). Without usage, every kWh is valued at the tariff. With usage and no export rate, exports are credited at the tariff (labelled as an assumption).

  5. Payback. First (fractional) year when cumulative savings reach the net cost; simple payback = net cost / year-1 savings; NPV discounts year n by 1.04^(n-1). Recommended option = highest NPV.

  6. Verdict on the recommended option: pays-back-early (within half the lifetime), pays-back-late (within the lifetime), does-not-pay-back, needs-prices (no tariff or install cost), no-usable-layout (the API returned no layouts: fewer than 4 panels fit).

The constants 0.85 (DC to AC derate), 0.995 (yearly output factor), 1.022 (yearly price increase), 1.04 (discount rate) and the 20-year lifespan are the values Google's US costs and savings page lists for its own US analysis. They are labelled google-us-method in every output and can be overridden. The lifetime defaults to the response's panelLifetimeYears when present.

No invented prices

Tariff, install cost and incentives have no hidden defaults. Without them you get sizes and energy and the verdict needs-prices. use_example_inputs / --example-inputs fills the missing prices with round placeholder values (0.15 per kWh, 3.00 per W, no incentive) that this lab chose for demos; every such input carries source: "example" and the note "EXAMPLE INPUT: placeholder chosen by this lab, not a market price or quote", and the verdict headline starts with "On EXAMPLE inputs (not real prices)".

Output

JSON (proposal): location, building (name, imagery quality and dates, distance from the query point), roof (max panels and kW, panel model, sunshine, segments with facing/pitch/area), inputs (each {value, source, note}), options[] (size, DC/AC energy, kWh/kW, CO2 offset from carbonOffsetFactorKgPerMwh, self-consumed/exported kWh, finance), recommended, verdict, apiFinancials (compacted analyses per bill: cash, financed, lease, incentives, implied price per kWh and per watt; plus a comparison table for one bill), assumptions, notes, attribution, references, source. Markdown: verdict, roof table, options table, the API comparison, an inputs table with sources, and the attribution line. A full sample is in reports/solarscope-mcp.md.

Measured

Keyless run of the real MCP server (in-memory MCP client) over fixtures/synthetic/: invented buildings and invented API financial analyses in the response shape documented in Google's API reference. The numbers show what the tool does with that input, not anything about real roofs or prices. "MCP = service" checks that the tool's JSON and Markdown equal the service output for the same input. Produced by bun run report; full output in reports/solarscope-mcp.md and reports/solarscope-mcp.json.

Scenario

Input

Calls

Imagery

Layouts

Options

Recommended

Yr-1 AC kWh

Net cost

Payback (yrs)

API payback, same bill (yrs)

Verdict

Solar response bytes

Proposal JSON bytes

Markdown bytes

MCP = service

sizes only

address, no prices

2

HIGH

33

5

36 panels / 14.4 kW

14,680

-

-

11.3 (20 panels)

needs-prices

37402

14922

5702

true

example inputs

address, use_example_inputs

2

HIGH

33

5

9 panels / 3.6 kW

4,208

10,800

15.2

11.3 (20 panels)

pays-back-late

37402

16616

6181

true

typed prices

address, 0.25/kWh, 2.80/W, 10%

2

HIGH

33

5

36 panels / 14.4 kW

14,680

36,288

9.2

11.3 (20 panels)

pays-back-early

37402

16218

5789

true

typed prices + usage

same + 6000 kWh/yr, export 0.05

2

HIGH

33

6

12 panels / 4.8 kW

5,572

12,096

8.2

11.3 (20 panels)

pays-back-early

37402

16635

5686

true

flat roof, MEDIUM imagery

address, min_quality MEDIUM, example inputs

2

MEDIUM

15

4

18 panels / 7.2 kW

9,360

21,600

13.8

-

pays-back-late

7971

7912

4495

true

shed (3 panels fit)

lat/lng, typed prices

1

HIGH

0

0

-

-

-

-

-

no-usable-layout

1726

4190

3177

true

  • Google's US method page says the 20-year cost without solar is 204.35 x monthlyBill; src/finance.ts with the page's constants (1.022 / 1.04, 20 years) gives 204.35.

  • 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): tool error google_PERMISSION_DENIED, reason SERVICE_DISABLED.

  • Point with no building: google_NOT_FOUND.

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

  • "example inputs" rows: 4 inputs carry source: "example"; "typed prices" rows: 0.

Live status (2026-10-21)

Calls made with our Maps Platform key in project dev-test-461117 while building this lab (bun test tests/live):

Call

Result

Geocoding API v4 geocode/address (public address of Google's HQ)

HTTP 200, coordinates in the expected box

Solar API buildingInsights:findClosest

HTTP 403, reason SERVICE_DISABLED: the API is not enabled in that project, and we did not enable it. tests/live prints a skipped: line

So no real Solar API response has been observed by this lab. The Solar path is tested only against synthetic responses built from the reference. When the API is enabled, bun run record saves one real response to the gitignored .recorded-local/, and tests/live then checks it against src/schema.ts.

What we checked in Google's docs

Checked on 2026-10-21.

Item

What the docs say

Source

Endpoint

GET https://solar.googleapis.com/v1/buildingInsights:findClosest, empty body; NOT_FOUND when no building is within about 50 m

findClosest reference (last updated 2026-05-12)

Parameters

location (required), requiredQuality (minimum; omitted = HIGH only), exactQualityRequired, experiments[] (pre-GA), additionalInsights[] (DETECTED_ARRAYS)

same

Imagery quality

HIGH, MEDIUM, LOW (plus IMAGERY_QUALITY_UNSPECIFIED); if only higher quality exists the API returns it, if nothing meets the minimum it answers 404

ImageryQuality, building insights guide

Response

name, center, boundingBox, imageryDate, imageryProcessedDate, postalCode, administrativeArea, statisticalArea, regionCode, solarPotential, imageryQuality, detectedArrays

findClosest reference

solarPotential

maxArrayPanelsCount, panelCapacityWatts, panelHeightMeters, panelWidthMeters, panelLifetimeYears, maxArrayAreaMeters2, maxSunshineHoursPerYear, carbonOffsetFactorKgPerMwh, wholeRoofStats, buildingStats, roofSegmentStats[], solarPanels[], solarPanelConfigs[] (only when at least 4 panels fit; config N = first N panels), financialAnalyses[] (empty where the API lacks data)

same

financialAnalyses

per monthlyBill: defaultBill, averageKwhPerMonth, panelConfigIndex (-1 = no layout), financialDetails, cashPurchaseSavings (outOfPocketCost, upfrontCost, rebateValue, paybackYears, negative = never), financedPurchaseSavings, leasingSavings, all money as Money (units int64 string + nanos)

same

US method constants

DC to AC 0.85, efficiency depreciation 0.995, cost increase 1.022, discount 1.04, 20-year lifespan; cost without solar = 204.35 x monthlyBill (reproduced in tests/unit/finance.test.ts)

Calculate costs and savings (US only)

Attribution

"Source: Includes solar data from Google" next to the data; "Includes data from Google Maps" for new content created from Environment API data

Solar API policies

"Installers lack fast, credible rooftop proposals"

Not verified: the idea's market claim, not measured

none

Doc inconsistencies we found: the building insights guide's TypeScript sample types imageryQuality as 'HIGH' | 'MEDIUM' | 'BASE' and sends required_quality: 'BASE', while the reference enum lists LOW. This lab follows the reference (LOW); we could not test which spelling the live API accepts. The US method page's LifetimeProductionAcKwh formula multiplies by dcToAcDerate although initialAcKwhPerYear already includes it; this lab applies the derate once. The guide's examples pass the key as key=; this lab sends X-Goog-Api-Key instead (the Solar API answered SERVICE_DISABLED for our project with that header, so it identified the project, but we have not seen a successful response with it).

Attribution and usage requirements

From the Solar API policies:

  • Attribution. If you use data or imagery from the Solar API you must show "Source: Includes solar data from Google" on or next to it. Content you create from Environment API data uses "Includes data from Google Maps" (text attribution rules: font, size, color, placement, translate="no" in HTML). Every JSON proposal carries both (attribution, derivedAttribution) and every Markdown proposal ends with both. Keep them next to the numbers wherever you show them.

  • No caching. Pre-fetching, caching or storing Solar content is generally restricted (place IDs are exempt). This server keeps nothing between calls, and the repo commits no Google responses.

  • Maps. If you show Solar results on a map, it must be a Google Map.

  • Terms. Your app needs public Terms of Use and a Privacy Policy that meet Google's requirements; use is governed by your agreement with Google. EEA billing addresses get different terms, and the docs say certain Solar API content is no longer returned for them.

  • Billing. Building insights is the "Solar API Building Insights" SKU (Essentials category) per the usage and billing page. No prices are quoted here; set Cloud quotas and budget alerts.

Limits

  • Solar responses unverified live. See Live status. Synthetic fixtures follow the reference field by field (src/types.ts, strict check in src/schema.ts), but real responses may carry fields or value formats we have not seen.

  • Not a quote, not financial advice. The proposal is arithmetic on the API's energy figures and your inputs: one flat tariff, one cost per watt, your incentives. No time-of-use rates, tiered tariffs, export caps, loans, maintenance, inverter replacement, taxes, shading changes or panel model choice. The API's own analysis uses its own data and models; differences between the two columns are expected.

  • No tariff data. The idea mentioned a local tariff table; there is no verified open data source for it in this MVP, so the tariff is a user input.

  • No weather. The idea's original title mentions the Weather API. It is not enabled in our project and is not needed for yearly energy (the Solar API already returns yearly DC kWh per layout), so it is left out.

  • Closest building. The API returns the building nearest the point; the output reports the distance and adds a note above 30 m. Geocoding returns one point per address, which can land on the wrong building.

  • 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. Fixture output is never a real building and says so in the Markdown footer.

  • Not included (separate ideas): detected arrays (additionalInsights=DETECTED_ARRAYS), roof geometry views, the dataLayers endpoint (imagery GeoTIFFs), the pre-GA EXPANDED_COVERAGE experiment, batch address lists. findClosestUrl already accepts additionalInsights and experiments, and src/types.ts types detectedArrays, so these can be added as new tools.

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 (SOLARSCOPE_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

Not affiliated

Not affiliated with, endorsed by, or sponsored by Google LLC. 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
    A
    quality
    D
    maintenance
    Enables solar energy feasibility analysis and ROI calculation for Indian users, including irradiance lookup, system sizing, cost estimation, subsidy calculation, and environmental impact assessment.
    8
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables solar energy feasibility analysis and ROI calculation for Indian users, including irradiance lookup, capacity estimation, subsidy calculation, and environmental impact assessment.
    -
  • 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