MCP for AI agents

Let an AI agent, such as Claude, get solar output estimates for Swedish roofs, and read your organisation's prospects and offers in Numsolar.

What it is

MCP (Model Context Protocol) is an open standard that AI agents use to call tools on other services. Numsolar is an MCP server: you give your agent our address, and the agent can then call our tools by itself when a question needs them, for example "How much power do 9 kWp on a south roof in Stockholm give in a year?" or "Which of our offers did the client accept this week?".

Technically: JSON-RPC 2.0 over HTTP, one POST per call, no streaming. The methods are initialize, ping, tools/list and tools/call. A plain-text version of this page for AI agents is at /llms.txt.

The public tools give estimates, not quotes. A real price needs an installer who checks the roof. Tax deduction follows the Skatteverket rules.

Units and conventions

  • Energy in kWh; _kwh_kwp is kWh per kWp of panels.
  • Panel power in kWp (equipment: panels in W, batteries in kWh, inverters in kW).
  • Angles in degrees. tilt_deg: 0 = flat, 90 = vertical.
  • Azimuth (azimuth_deg) is the compass direction the roof face points to: 0 = north, 90 = east, 180 = south, 270 = west, clockwise.
  • Coordinates (lat, lng) are WGS 84 decimal degrees. For a Swedish town use its centre: the data grid is 0.25° × 0.5°, so a town centre is accurate enough for an estimate. get_started_link also takes a street address.
  • The REST API /api/v1/pv-yield calls the angles tilt and azimuth; the numbers are the same.
  • Money in SEK incl. VAT.
  • A result is JSON in result.content[0].text, with the key numbers first. The same object is in result.structuredContent.

Two endpoints

POST /mcp/public

Public estimates (no key). Anonymous. Solar output estimates for any roof in Sweden, and a link where a homeowner continues. Uses only our own precomputed data: no account and no cost. We keep only a short call log, without the values you send.

URL: https://numsolar.com/mcp/public. Its tools:

POST /mcp

Your organisation's data (key needed). Needs an API key of your organisation. Read only: your prospects, offers, equipment and price templates, plus the public estimate tools. A key on /mcp gives all 10 tools, so your agent needs only this one server. A key never sees another organisation's data.

URL: https://numsolar.com/mcp. Its tools:

Get a key

Only /mcp needs a key. /mcp/public works without one.

  1. Open the settings page. Sign in to Numsolar as an admin of your organisation and open Settings › API keys for AI agents. Open API keys for AI agents
  2. Create a key. Click Create key. Give the key a name (for example "Our sales agent") and tick what it may read.
  3. Copy the key into your agent. Copy the key at once: we show it only once. Your agent sends it in the header Authorization: Bearer ns_live_XXXXXXXX_YOUR_SECRET_KEY. Revoke the key on the same page when you no longer need it.

What a key may read

You tick one or more of these when you create the key. A tool that needs a scope the key does not have answers insufficient_scope.

Scope What the agent may read
prospects:read Prospects: name, contact details, address, status (tools list_prospects, get_prospect).
offers:read Offers: status, system size, price incl. VAT, tax deduction (tools list_offers, get_offer).
catalog:read Catalogue: equipment and price templates (tools list_equipment, list_price_templates).
prices:read Internal cost and margin: adds purchase cost, margin and discount to the offer and equipment tools. Give it only to an agent you trust with your purchase prices.
Keep the key secret, like a password: anyone with it can read what the key may read. Prospect data contains personal data; your organisation is responsible for the agent it gives a key to.

Connect

Claude Code and other agents that read an mcpServers config (for example .mcp.json): put your key in place of ns_live_XXXXXXXX_YOUR_SECRET_KEY.

Your organisation's data (/mcp)
{
  "mcpServers": {
    "numsolar": {
      "headers": {
        "Authorization": "Bearer ns_live_XXXXXXXX_YOUR_SECRET_KEY"
      },
      "type": "http",
      "url": "https://numsolar.com/mcp"
    }
  }
}
Claude Code (terminal)
claude mcp add --transport http numsolar https://numsolar.com/mcp --header "Authorization: Bearer ns_live_XXXXXXXX_YOUR_SECRET_KEY"

Public estimates only, no key. Apps that connect by URL only (for example a custom connector in Claude Desktop) can use this address; /mcp needs an app that can send the Authorization header.

Public estimates (/mcp/public)
{
  "mcpServers": {
    "numsolar": {
      "type": "http",
      "url": "https://numsolar.com/mcp/public"
    }
  }
}

Try it with curl

curl /mcp/public
curl -s -X POST 'https://numsolar.com/mcp/public' \
  -H 'content-type: application/json' \
  -d '{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"azimuth_deg":180,"kwp":9.24,"lat":59.33,"lng":18.07,"tilt_deg":35},"name":"estimate_yield"}}'
curl /mcp
curl -s -X POST 'https://numsolar.com/mcp' \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer ns_live_XXXXXXXX_YOUR_SECRET_KEY' \
  -d '{"id":1,"jsonrpc":"2.0","method":"tools/call","params":{"arguments":{"limit":2},"name":"list_prospects"}}'

An agent starts with initialize. The answer tells it what the server is and how to read the numbers:

initialize request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "my-agent", "version": "1.0"}}
}
200 OK
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "capabilities": {
      "tools": {"listChanged": false}
    },
    "instructions": "Numsolar estimates solar PV output for Swedish roofs and gives installers access to their own prospects and offers. Public tools are estimates, not quotes. Coordinates are WGS 84 decimal degrees; a town centre is accurate enough. get_started_link also takes a street address. Results are JSON in content[0].text. Money is SEK incl. VAT. Azimuth: 0 = north, 90 = east, 180 = south, clockwise. Status filters take the organisation's own status names, as list_* shows them.",
    "protocolVersion": "2025-06-18",
    "serverInfo": {"name": "numsolar", "version": "0.4.0"}
  }
}

Tools

Call a tool with tools/call. The answer has the result as JSON text in result.content[0].text; the examples below show that JSON.

estimate_yield

Estimate yearly and monthly PV output in kWh for one roof face in Sweden (nearby land in Norway, Denmark and Finland also gets a value, with lower accuracy). Returns kWh per kWp of panels (and total kWh when kwp is given), the best angle at the point and the data source. Angles in degrees: tilt_deg 0 = flat, 90 = vertical; azimuth_deg is a compass bearing, 0 = north, 90 = east, 180 = south, 270 = west (clockwise). An estimate, not a quote. lat and lng are WGS 84 decimal degrees; for a Swedish town its centre is accurate enough (the data grid is 0.25° × 0.5°).

Endpoint: /mcp/public. Also on /mcp with a key. No key needed.

Argument Type Description
azimuth_degrequired number Direction the roof face points, in compass degrees from 0 to 360 (0 = north, 90 = east, 180 = south, 270 = west).
latrequired number Latitude in degrees, e.g. 59.33 (Stockholm).
lngrequired number Longitude in degrees, e.g. 18.07 (Stockholm).
tilt_degrequired number Roof tilt in degrees, from 0 (flat) to 90 (vertical).
kwp number Panel power on this face in kWp (optional); adds total kWh.
shading number Share of sunlight that reaches the panels, 0–1. Default 1.0 (no shade).
system_loss number Fraction of energy lost in cables and inverter, 0–0.6. Default 0.14.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "estimate_yield", "arguments": {"lat": 59.33, "lng": 18.07, "tilt_deg": 35, "azimuth_deg": 180, "kwp": 9.24}}
}
Result (the JSON in content[0].text)
{
  "annual_kwh": 9084.8,
  "annual_kwh_kwp": 983.2,
  "monthly_kwh": [162.2, 337.4, 824.5, 1127.6, 1276.1, 1330.5, 1262.0, 1109.9, 851.1, 501.4, 193.1, 109.3],
  "monthly_kwh_kwp": [17.5, 36.5, 89.2, 122.0, 138.1, 144.0, 136.6, 120.1, 92.1, 54.3, 20.9, 11.8],
  "optimal": {"tilt_deg": 45, "azimuth_deg": 180, "annual_kwh_kwp": 988.7},
  "disclaimer": "Estimate, not a quote. Tax deduction follows Skatteverket rules.",
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "input": {"lat": 59.33, "lng": 18.07, "tilt_deg": 35.0, "azimuth_deg": 180.0, "kwp": 9.24, "shading": 1.0, "system_loss": 0.14},
  "cell": {
    "lat": 59.25,
    "lng": 18.0,
    "elevation_m": 48.0,
    "era5_corrected": false,
    "grid_deg": [0.25, 0.5],
    "interpolation": "bilinear",
    "radiation_db": "PVGIS-SARAH3"
  },
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "radiation_db": "PVGIS-SARAH3",
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "model": {"albedo": 0.2, "anchor_scale": 0.978, "horizon": "free", "poa_model": "v2", "representative_days": 12, "sky_model": "hay_davies", "steps_per_day": 24, "system_loss_default": 0.14},
  "ghi_annual_kwh_m2": 995.5,
  "orientation_factor": 0.994,
  "poa_annual_kwh_m2": 1171.5
}

estimate_roof_faces

Estimate the total yearly and monthly PV output in kWh for a house with 1 to 8 roof faces in Sweden, and the output of each face. Each face has tilt_deg (0 = flat, 90 = vertical), azimuth_deg (compass degrees: 0 = north, 90 = east, 180 = south, 270 = west, clockwise) and kwp (panel power in kWp). An estimate, not a quote. lat and lng are WGS 84 decimal degrees; for a Swedish town its centre is accurate enough (the data grid is 0.25° × 0.5°).

Endpoint: /mcp/public. Also on /mcp with a key. No key needed.

Argument Type Description
facesrequired list of objects 1 to 8 roof faces with panels, each with tilt_deg, azimuth_deg and kwp.
faces[].azimuth_degrequired number Direction the roof face points, in compass degrees from 0 to 360 (0 = north, 90 = east, 180 = south, 270 = west).
faces[].kwprequired number Panel power on this face in kWp.
faces[].tilt_degrequired number Roof tilt in degrees, from 0 (flat) to 90 (vertical).
latrequired number Latitude in degrees, e.g. 59.33 (Stockholm).
lngrequired number Longitude in degrees, e.g. 18.07 (Stockholm).
shading number Share of sunlight that reaches the panels, 0–1. Default 1.0 (no shade).
system_loss number Fraction of energy lost in cables and inverter, 0–0.6. Default 0.14.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "estimate_roof_faces", "arguments": {"lat": 57.71, "lng": 11.97, "faces": [{"tilt_deg": 27, "azimuth_deg": 90, "kwp": 4.2}, {"tilt_deg": 27, "azimuth_deg": 270, "kwp": 4.2}]}}
}
Result (the JSON in content[0].text)
{
  "annual_kwh": 6322.8,
  "monthly_kwh": [74.0, 178.3, 478.5, 805.5, 1027.4, 1045.8, 985.8, 782.1, 534.6, 266.5, 96.6, 47.4],
  "faces": [
    {
      "annual_kwh": 3160.2,
      "annual_kwh_kwp": 752.4,
      "monthly_kwh": [37.9, 89.6, 233.4, 407.3, 509.0, 523.2, 493.6, 391.9, 267.2, 132.4, 49.9, 24.4],
      "monthly_kwh_kwp": [9.0, 21.3, 55.6, 97.0, 121.2, 124.6, 117.5, 93.3, 63.6, 31.5, 11.9, 5.8],
      "optimal": {"tilt_deg": 40, "azimuth_deg": 180, "annual_kwh_kwp": 958.4},
      "input": {"lat": 57.71, "lng": 11.97, "tilt_deg": 27.0, "azimuth_deg": 90.0, "kwp": 4.2, "shading": 1.0, "system_loss": 0.14},
      "cell": {
        "lat": 57.75,
        "lng": 12.0,
        "elevation_m": 4.0,
        "era5_corrected": false,
        "grid_deg": [0.25, 0.5],
        "interpolation": "bilinear",
        "radiation_db": "PVGIS-SARAH3"
      },
      "ghi_annual_kwh_m2": 987.5,
      "orientation_factor": 0.785,
      "poa_annual_kwh_m2": 900.4
    },
    {
      "annual_kwh": 3162.6,
      "annual_kwh_kwp": 753.0,
      "monthly_kwh": [36.1, 88.8, 245.1, 398.3, 518.4, 522.6, 492.2, 390.2, 267.4, 134.1, 46.7, 23.0],
      "monthly_kwh_kwp": [8.6, 21.1, 58.4, 94.8, 123.4, 124.4, 117.2, 92.9, 63.7, 31.9, 11.1, 5.5],
      "optimal": {"tilt_deg": 40, "azimuth_deg": 180, "annual_kwh_kwp": 958.4},
      "input": {"lat": 57.71, "lng": 11.97, "tilt_deg": 27.0, "azimuth_deg": 270.0, "kwp": 4.2, "shading": 1.0, "system_loss": 0.14},
      "cell": {
        "lat": 57.75,
        "lng": 12.0,
        "elevation_m": 4.0,
        "era5_corrected": false,
        "grid_deg": [0.25, 0.5],
        "interpolation": "bilinear",
        "radiation_db": "PVGIS-SARAH3"
      },
      "ghi_annual_kwh_m2": 987.5,
      "orientation_factor": 0.786,
      "poa_annual_kwh_m2": 901.2
    }
  ],
  "disclaimer": "Estimate, not a quote. Tax deduction follows Skatteverket rules.",
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "input": {"lat": 57.71, "lng": 11.97, "shading": 1.0, "system_loss": 0.14},
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "model": {"albedo": 0.2, "anchor_scale": 0.978, "horizon": "free", "poa_model": "v2", "representative_days": 12, "sky_model": "hay_davies", "steps_per_day": 24, "system_loss_default": 0.14}
}

yield_matrix

Return yearly PV output in kWh per kWp for every roof angle at one point in Sweden, and the best angle. Rows are tilts 0–90 degrees in 5-degree steps (0 = flat); columns are compass azimuths 0–350 degrees in 10-degree steps (0 = north, 90 = east, 180 = south, 270 = west, clockwise). Limited to 10 calls per clock minute; use estimate_yield for one angle. lat and lng are WGS 84 decimal degrees; for a Swedish town its centre is accurate enough (the data grid is 0.25° × 0.5°).

Endpoint: /mcp/public. Also on /mcp with a key. No key needed.

Argument Type Description
latrequired number Latitude in degrees, e.g. 59.33 (Stockholm).
lngrequired number Longitude in degrees, e.g. 18.07 (Stockholm).
shading number Share of sunlight that reaches the panels, 0–1. Default 1.0 (no shade).
system_loss number Fraction of energy lost in cables and inverter, 0–0.6. Default 0.14.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "yield_matrix", "arguments": {"lat": 59.33, "lng": 18.07}}
}
Result (the JSON in content[0].text), 2 of 19 rows
{
  "annual_kwh_kwp": [
    [798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7],
    [753.5, 754.1, 756.0, 759.2, 763.5, 768.8, 775.0, 781.8, 789.2, 796.6, 804.2, 811.4, 818.2, 824.4, 829.7, 834.0, 837.2, 839.1, 839.8, 839.1, 837.2, 834.1, 829.7, 824.4, 818.2, 811.4, 804.1, 796.6, 789.1, 781.8, 775.0, 768.8, 763.5, 759.2, 756.1, 754.1]
  ],
  "optimal": {"tilt_deg": 45, "azimuth_deg": 180, "annual_kwh_kwp": 988.7},
  "tilts_deg": [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90],
  "azimuths_deg": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350],
  "disclaimer": "Estimate, not a quote. Tax deduction follows Skatteverket rules.",
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "input": {"lat": 59.33, "lng": 18.07, "shading": 1.0, "system_loss": 0.14},
  "cell": {
    "lat": 59.25,
    "lng": 18.0,
    "elevation_m": 48.0,
    "era5_corrected": false,
    "grid_deg": [0.25, 0.5],
    "interpolation": "bilinear",
    "radiation_db": "PVGIS-SARAH3"
  },
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "radiation_db": "PVGIS-SARAH3",
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "model": {"albedo": 0.2, "anchor_scale": 0.978, "horizon": "free", "poa_model": "v2", "representative_days": 12, "sky_model": "hay_davies", "steps_per_day": 24, "system_loss_default": 0.14}
}

Return a Numsolar web link where the homeowner continues with a real roof model and installer quotes. Give address (a street address in Sweden), or lat and lng in degrees. It creates no account and starts no calculation. The link opens the Numsolar wizard with the address filled in.

Endpoint: /mcp/public. Also on /mcp with a key. No key needed.

Argument Type Description
address string Street address in Sweden.
lat number Latitude in degrees (use with lng instead of address).
lng number Longitude in degrees (use with lat instead of address).
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "get_started_link", "arguments": {"address": "Storgatan 1, Uppsala"}}
}
Result (the JSON in content[0].text)
{
  "url": "https://numsolar.com/get-started?address=Storgatan+1%2C+Uppsala",
  "message": "Open this link in a browser: the homeowner checks the address, sees a 3D model of the roof and the estimated output, and can ask installers for quotes."
}

list_prospects

List your organisation's prospects (homeowners who may buy), newest change first, with name, city and status; use get_prospect for contact details and offers.

Endpoint: /mcp. Scope: prospects:read

Argument Type Description
cursor string next_cursor from the previous page; leave out for the first page.
limit integer Rows per page, 1 to 100 (default 20).
status string Only prospects with this status name, as shown in Numsolar.
updated_since string Only rows changed at or after this time, ISO 8601 (for example 2026-11-01 or 2026-11-01T08:00:00Z), UTC.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "list_prospects", "arguments": {"limit": 2}}
}
Result (the JSON in content[0].text)
{
  "items": [
    {
      "city": "Uppsala",
      "id": "00000000-0000-4000-8000-000000000001",
      "name": "Anna Svensson",
      "number": "P-1024",
      "status": "Offer sent",
      "updated_at": "2026-11-03T09:30:00Z"
    },
    {
      "city": "Västerås",
      "id": "00000000-0000-4000-8000-000000000003",
      "name": "Erik Holm",
      "number": "P-1023",
      "status": "New",
      "updated_at": "2026-11-02T14:05:00Z"
    }
  ],
  "next_cursor": "WyIyMDI2LTExLTAzVDA5OjMwOjAwWiIsIjAwMDAiXQ",
  "truncated": false
}

get_prospect

Get one prospect of your organisation by id, with name, e-mail, phone, address and status, and its offers when the key may read offers.

Endpoint: /mcp. Scope: prospects:read

Argument Type Description
idrequired string The prospect id from list_prospects.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "get_prospect", "arguments": {"id": "00000000-0000-4000-8000-000000000001"}}
}
Result (the JSON in content[0].text)
{
  "address": "Storgatan 1",
  "city": "Uppsala",
  "co_customer_name": null,
  "co_customer_ssn": null,
  "country": "Sweden",
  "created_at": "2026-10-28T08:12:00Z",
  "email": "anna.svensson@example.com",
  "first_name": "Anna",
  "id": "00000000-0000-4000-8000-000000000001",
  "last_name": "Svensson",
  "number": "P-1024",
  "offers": [
    {
      "currency": "SEK",
      "id": "00000000-0000-4000-8000-000000000002",
      "kwp": 10.4,
      "number": "O-2031",
      "price_after_deduction": 136000.0,
      "prospect_id": "00000000-0000-4000-8000-000000000001",
      "status": "Sent",
      "title": "Villa Svensson, 10.4 kWp",
      "updated_at": "2026-11-03T09:30:00Z"
    }
  ],
  "phone": "+46 70 000 00 00",
  "property_identifier": null,
  "ssn": null,
  "status": "Offer sent",
  "updated_at": "2026-11-03T09:30:00Z",
  "zip": "753 20"
}

list_offers

List your organisation's offers, newest change first, with status, system size in kWp and price after the tax deduction in SEK incl. VAT; use get_offer for the details.

Endpoint: /mcp. Scope: offers:read

Argument Type Description
cursor string next_cursor from the previous page; leave out for the first page.
limit integer Rows per page, 1 to 100 (default 20).
status string Only offers with this status name, as shown in Numsolar and in each offer's status.
updated_since string Only rows changed at or after this time, ISO 8601 (for example 2026-11-01 or 2026-11-01T08:00:00Z), UTC.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "list_offers", "arguments": {"limit": 2}}
}
Result (the JSON in content[0].text)
{
  "items": [
    {
      "currency": "SEK",
      "id": "00000000-0000-4000-8000-000000000002",
      "kwp": 10.4,
      "number": "O-2031",
      "price_after_deduction": 136000.0,
      "prospect_id": "00000000-0000-4000-8000-000000000001",
      "status": "Sent",
      "title": "Villa Svensson, 10.4 kWp",
      "updated_at": "2026-11-03T09:30:00Z"
    },
    {
      "currency": "SEK",
      "id": "00000000-0000-4000-8000-000000000004",
      "kwp": 8.2,
      "number": "O-2030",
      "price_after_deduction": 112500.0,
      "prospect_id": "00000000-0000-4000-8000-000000000003",
      "status": "New",
      "title": "Villa Holm, 8.2 kWp",
      "updated_at": "2026-11-03T09:30:00Z"
    }
  ],
  "next_cursor": "WyIyMDI2LTExLTAzVDA5OjMwOjAwWiIsIjAwMDAiXQ",
  "truncated": false
}

get_offer

Get one offer of your organisation by id: status, package and items, system size in kWp, estimated yearly output in kWh, price after the tax deduction, the deduction and VAT in SEK incl. VAT, and the client's share link.

Endpoint: /mcp. Scope: offers:read

Argument Type Description
idrequired string The offer id from list_offers.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "get_offer", "arguments": {"id": "00000000-0000-4000-8000-000000000002"}}
}
Result (the JSON in content[0].text)
{
  "approved": false,
  "cost_of_goods": 78000.0,
  "currency": "SEK",
  "deduction": 24000.0,
  "discount": 0.0,
  "id": "00000000-0000-4000-8000-000000000002",
  "items": [
    {
      "category": "solar_panel",
      "cost": 1450.0,
      "quantity": 24.0,
      "title": "Solar panel 435 W"
    },
    {
      "category": "inverter",
      "cost": 14500.0,
      "quantity": 1.0,
      "title": "Inverter 10 kW"
    },
    {
      "category": "installation",
      "cost": 22000.0,
      "quantity": 1.0,
      "title": "Installation"
    }
  ],
  "kwp": 10.44,
  "margin": 34000.0,
  "number": "O-2031",
  "offer_date": "2026-11-03",
  "package": {
    "id": "00000000-0000-4000-8000-000000000005",
    "title": "Solar 10 kWp"
  },
  "price_after_deduction": 136000.0,
  "prospect_id": "00000000-0000-4000-8000-000000000001",
  "prospect_name": "Anna Svensson",
  "share_url": "https://numsolar.com/o/EXAMPLESHARETOKEN",
  "status": "Sent",
  "tax": 0.0,
  "title": "Villa Svensson, 10.4 kWp",
  "updated_at": "2026-11-03T09:30:00Z",
  "valid_until": "2026-12-03",
  "vat": 32000.0,
  "yearly_kwh": 9650.0
}

list_equipment

List your organisation's equipment catalogue, newest change first, with panel power in W, battery capacity in kWh and inverter power in kW; the purchase cost needs the prices:read scope.

Endpoint: /mcp. Scope: catalog:read

Argument Type Description
cursor string next_cursor from the previous page; leave out for the first page.
limit integer Rows per page, 1 to 100 (default 20).
updated_since string Only rows changed at or after this time, ISO 8601 (for example 2026-11-01 or 2026-11-01T08:00:00Z), UTC.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "list_equipment", "arguments": {"limit": 2}}
}
Result (the JSON in content[0].text)
{
  "items": [
    {
      "active": true,
      "battery_kwh": null,
      "category": "solar_panel",
      "cost": 1450.0,
      "currency": "SEK",
      "description": "Monocrystalline, black frame",
      "id": "00000000-0000-4000-8000-000000000007",
      "inverter_kw": null,
      "panel_w": 435,
      "title": "Solar panel 435 W",
      "updated_at": "2026-11-03T09:30:00Z",
      "warranty_years": 25
    },
    {
      "active": true,
      "battery_kwh": 10.0,
      "category": "battery",
      "cost": 42000.0,
      "currency": "SEK",
      "description": null,
      "id": "00000000-0000-4000-8000-000000000008",
      "inverter_kw": null,
      "panel_w": null,
      "title": "Battery 10 kWh",
      "updated_at": "2026-11-03T09:30:00Z",
      "warranty_years": 10
    }
  ],
  "next_cursor": "WyIyMDI2LTExLTAzVDA5OjMwOjAwWiIsIjAwMDAiXQ",
  "truncated": false
}

list_price_templates

List your organisation's price templates (the price rules an offer uses), newest change first, with status draft or active and currency.

Endpoint: /mcp. Scope: catalog:read

Argument Type Description
cursor string next_cursor from the previous page; leave out for the first page.
limit integer Rows per page, 1 to 100 (default 20).
status string: draft or active Only templates with this status: draft or active.
updated_since string Only rows changed at or after this time, ISO 8601 (for example 2026-11-01 or 2026-11-01T08:00:00Z), UTC.
Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "list_price_templates", "arguments": {"limit": 2}}
}
Result (the JSON in content[0].text)
{
  "items": [
    {
      "currency": "SEK",
      "id": "00000000-0000-4000-8000-000000000006",
      "package_id": "00000000-0000-4000-8000-000000000005",
      "status": "active",
      "title": "Standard Sweden",
      "updated_at": "2026-11-03T09:30:00Z",
      "version": 3
    }
  ],
  "next_cursor": null,
  "truncated": false
}

Errors

When a tool cannot answer, the HTTP status is 200 and result.isError is true. The JSON text has code, a one-sentence message that says how to fix the call and, when one argument is wrong, field:

Request (tilt 120°)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {"name": "estimate_yield", "arguments": {"lat": 59.33, "lng": 18.07, "tilt_deg": 120, "azimuth_deg": 180}}
}
Result, isError: true
{
  "code": "invalid_args",
  "message": "tilt_deg must be a roof tilt in degrees from 0 (flat) to 90 (vertical).",
  "field": "tilt_deg"
}
Code Status Retry-After When
invalid_args 200, isError – An argument is missing, has the wrong type or is out of range. field names it (for example tilt_deg or faces.1.kwp) and message says the allowed values.
out_of_coverage 200, isError – The point is outside the area with yield data (latitude 55.0–69.5, longitude 10.5–24.5, land only). The data is made for Sweden; nearby land in Norway, Denmark and Finland also gets a value, with lower accuracy.
unknown_tool 200, isError – No tool with this name on this endpoint. The organisation tools are only on /mcp; call tools/list for the names.
insufficient_scope 200, isError – The key may not use this tool. message names the missing scope: create a key that may read it.
not_found 200, isError – No record with this id in your organisation. An id of another organisation also gives not_found.
rate_limited 200, isError retry_after_seconds Over a limit (see Limits). Nothing was done. Wait retry_after_seconds, then call again.
busy 200, isError retry_after_seconds The server runs its maximum of calls at once, or the yield data is still loading. Nothing was done. Wait retry_after_seconds.
disabled 503 – The server for AI agents is switched off. HTTP 503 with the JSON-RPC error -32000 and data.code disabled.
internal_error 200, isError – Something failed on our side. The error has request_id: send it to us if the error stays.

Errors before a tool runs

These are JSON-RPC errors (error.code, error.message) or HTTP statuses. A request without an id is a notification: the answer is HTTP 202 with no body.

Code Status Retry-After When
-32700 400 – The body is not valid JSON.
-32600 200 – The body is not one JSON-RPC 2.0 request object, or it is a batch (a JSON array). Send one request per HTTP POST.
-32601 200 – Unknown method. The methods are initialize, ping, tools/list and tools/call.
-32602 200 – tools/call without params.name, or params.arguments is not an object.
-32001 401 – /mcp only: the key is missing, wrong or revoked, or its organisation is closed. The answer has WWW-Authenticate: Bearer and data.code unauthorized.
-32001 429 seconds until the minute ends /mcp only: 20 calls with a wrong key from your IP address in one clock minute; a call with a valid key still works. data.code is rate_limited.
-32001 429 seconds until the minute ends Too many requests of any kind from your IP address in one clock minute (see Limits). data.code is rate_limited.
-32600 405 – A GET, PUT, PATCH, DELETE (or OPTIONS on /mcp). This server does not stream (no SSE): use POST. The Allow header lists the methods.
-32600 413 – The body is larger than 256 KB.
-32000 503 – The server for AI agents is switched off (data.code disabled).

Limits

  • Requests per IP address: 120 requests per clock minute on /mcp/public and 600 on /mcp, of any kind (also initialize, tools/list, ping and bad requests). Over that: HTTP 429 with Retry-After.
  • Per IP address on /mcp/public: 60 tool calls per clock minute and 1000 a day (UTC). yield_matrix: also at most 10 per clock minute.
  • Per key on /mcp: 120 tool calls per clock minute and 10000 a day (UTC), the public tools included (yield_matrix: also at most 10 per clock minute). Each key has its own count: your other keys keep working.
  • Calls with a wrong key: 20 per clock minute per IP address. After that, a call from that address with a missing or wrong key gets HTTP 429 until the minute ends; a call with a valid key still works.
  • Calls at the same time: 8 tool calls at once for the whole server. Over that, a call gets busy with retry_after_seconds.
  • Request size: 256 KB per request body. A larger body gets HTTP 413.

Minute limits restart at each clock minute (:00). Over a limit, a tool call answers rate_limited with retry_after_seconds, and nothing is done. Need more? Write to contact@numsolar.com.

A plain-text summary of this page for AI agents: /llms.txt.