greenpick 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {greenpick-0.4.0/src/greenpick.egg-info → greenpick-0.4.2}/PKG-INFO +10 -9
  2. {greenpick-0.4.0 → greenpick-0.4.2}/README.md +8 -7
  3. {greenpick-0.4.0 → greenpick-0.4.2}/pyproject.toml +2 -2
  4. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/__init__.py +1 -1
  5. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/annual.json +3 -1
  6. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/regions.json +1 -1
  7. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/server.py +27 -16
  8. {greenpick-0.4.0 → greenpick-0.4.2/src/greenpick.egg-info}/PKG-INFO +10 -9
  9. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/requires.txt +1 -1
  10. {greenpick-0.4.0 → greenpick-0.4.2}/tests/test_services.py +68 -4
  11. {greenpick-0.4.0 → greenpick-0.4.2}/LICENSE +0 -0
  12. {greenpick-0.4.0 → greenpick-0.4.2}/setup.cfg +0 -0
  13. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/__main__.py +0 -0
  14. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/config.py +0 -0
  15. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/__init__.py +0 -0
  16. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/_net.py +0 -0
  17. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/auto_repository.py +0 -0
  18. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/canada_repository.py +0 -0
  19. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/eia_repository.py +0 -0
  20. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
  21. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/emission_factors.json +0 -0
  22. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/entsoe_repository.py +0 -0
  23. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/gpu_power.json +0 -0
  24. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/inference_energy.json +0 -0
  25. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/instance_families.json +0 -0
  26. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/mock_repository.py +0 -0
  27. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/network.json +0 -0
  28. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/power.json +0 -0
  29. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/pricing.json +0 -0
  30. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/repository.py +0 -0
  31. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/static_data.py +0 -0
  32. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/typical.json +0 -0
  33. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/data/typical_repository.py +0 -0
  34. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/models.py +0 -0
  35. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/__init__.py +0 -0
  36. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/carbon_service.py +0 -0
  37. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/cost_service.py +0 -0
  38. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/data_hint.py +0 -0
  39. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/forecast_service.py +0 -0
  40. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/inference_service.py +0 -0
  41. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/instance_service.py +0 -0
  42. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/latency_service.py +0 -0
  43. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/locality_service.py +0 -0
  44. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
  45. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/recommender.py +0 -0
  46. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/residency_service.py +0 -0
  47. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/rightsizing_service.py +0 -0
  48. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/scheduling_service.py +0 -0
  49. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/serving_service.py +0 -0
  50. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick/services/training_service.py +0 -0
  51. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
  52. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
  53. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/entry_points.txt +0 -0
  54. {greenpick-0.4.0 → greenpick-0.4.2}/src/greenpick.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: greenpick
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
5
5
  Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering
19
19
  Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: mcp<2,>=1.2
22
+ Requires-Dist: mcp<2,>=1.7
23
23
  Requires-Dist: pydantic>=2.0
24
24
  Requires-Dist: httpx>=0.27
25
25
  Provides-Extra: dev
@@ -60,11 +60,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
60
60
  > this month's average for each grid, from a year of real data.
61
61
 
62
62
  <p align="center">
63
- <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~30× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
63
+ <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
64
64
  </p>
65
65
 
66
- *The same 40-GPU-hour training run emits ~0.7 kg CO₂ in Stockholm vs ~19 kg in Sydney — a ~30×
67
- difference from region choice alone. Ask GreenPick and it surfaces that (with the cost and latency
66
+ *The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
67
+ difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
68
+ a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
68
69
  tradeoff) while you're still in the editor, before you commit.*
69
70
 
70
71
  **▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
@@ -195,12 +196,12 @@ See `docs/user-questions-backlog.md`.
195
196
  intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
196
197
  ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
197
198
  `quality: "typical"` — a realistic norm, never presented as a current reading.
198
- - **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
199
+ - **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
199
200
  yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
200
201
  CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
201
202
  comparing regions, but carries no timing signal.
202
- - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
203
- exists (currently Sydney).
203
+ - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
204
+ every other source is missing. No catalog region relies on it.
204
205
  - Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
205
206
  pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
206
207
  - Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
@@ -318,7 +319,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
318
319
  `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
319
320
 
320
321
  **Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
321
- compliance report. "Stockholm is ~30× cleaner than Sydney" is what you need to pick a region in
322
+ compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
322
323
  seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
323
324
  GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
324
325
 
@@ -30,11 +30,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
30
30
  > this month's average for each grid, from a year of real data.
31
31
 
32
32
  <p align="center">
33
- <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~30× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
33
+ <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
34
34
  </p>
35
35
 
36
- *The same 40-GPU-hour training run emits ~0.7 kg CO₂ in Stockholm vs ~19 kg in Sydney — a ~30×
37
- difference from region choice alone. Ask GreenPick and it surfaces that (with the cost and latency
36
+ *The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
37
+ difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
38
+ a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
38
39
  tradeoff) while you're still in the editor, before you commit.*
39
40
 
40
41
  **▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
@@ -165,12 +166,12 @@ See `docs/user-questions-backlog.md`.
165
166
  intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
166
167
  ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
167
168
  `quality: "typical"` — a realistic norm, never presented as a current reading.
168
- - **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
169
+ - **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
169
170
  yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
170
171
  CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
171
172
  comparing regions, but carries no timing signal.
172
- - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
173
- exists (currently Sydney).
173
+ - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
174
+ every other source is missing. No catalog region relies on it.
174
175
  - Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
175
176
  pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
176
177
  - Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
@@ -288,7 +289,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
288
289
  `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
289
290
 
290
291
  **Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
291
- compliance report. "Stockholm is ~30× cleaner than Sydney" is what you need to pick a region in
292
+ compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
292
293
  seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
293
294
  GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
294
295
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "greenpick"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Topic :: Scientific/Engineering",
20
20
  ]
21
21
  dependencies = [
22
- "mcp>=1.2,<2", # Model Context Protocol SDK (FastMCP lives here). Pinned <2: mcp 2.x
22
+ "mcp>=1.7,<2", # Model Context Protocol SDK (FastMCP lives here). >=1.7: tool annotations. Pinned <2: mcp 2.x
23
23
  # renamed FastMCP -> MCPServer and changed APIs; this code targets the 1.x API.
24
24
  "pydantic>=2.0",
25
25
  "httpx>=0.27", # async HTTP for live data sources
@@ -4,7 +4,7 @@ Greener cloud-region choices with honest carbon + cost + latency tradeoffs,
4
4
  delivered inside the vibe-coding loop. Directional guidance, not audited accounting.
5
5
  """
6
6
 
7
- __version__ = "0.4.0"
7
+ __version__ = "0.4.2"
8
8
 
9
9
  # httpx logs every request URL at INFO, and our live sources pass their API key / token as a URL
10
10
  # query parameter — so at the default log level the secret would land in the MCP client's logs.
@@ -8,6 +8,8 @@
8
8
  "JP": { "country": "Japan", "g_co2_per_kwh": 477.3, "year": 2025,
9
9
  "note": "National average; the Tokyo (TEPCO) area mix differs somewhat and varies by hour." },
10
10
  "IN": { "country": "India", "g_co2_per_kwh": 670.1, "year": 2025,
11
- "note": "National average; Maharashtra (Mumbai, Pune) runs about 6% above it on Ember's state-level data." }
11
+ "note": "National average; Maharashtra (Mumbai, Pune) runs about 6% above it on Ember's state-level data." },
12
+ "AU": { "country": "Australia", "g_co2_per_kwh": 525.2, "year": 2025,
13
+ "note": "National average; New South Wales (Sydney) is more coal-heavy and runs higher, so treat this as a lower bound for Sydney." }
12
14
  }
13
15
  }
@@ -6,7 +6,7 @@
6
6
  "us-west-2": { "name": "Oregon", "lat": 45.87, "lon": -119.69, "zone": "US-NW-PACW", "eia_ba": "BPAT", "jurisdiction": "US" },
7
7
  "eu-west-1": { "name": "Ireland", "lat": 53.41, "lon": -8.24, "zone": "IE", "entsoe_eic": "10Y1001A1001A59C", "jurisdiction": "EEA" },
8
8
  "eu-north-1": { "name": "Stockholm", "lat": 59.33, "lon": 18.07, "zone": "SE", "entsoe_eic": "10YSE-1--------K", "jurisdiction": "EEA" },
9
- "ap-southeast-2": { "name": "Sydney", "lat": -33.87, "lon": 151.21, "zone": "AU-NSW", "jurisdiction": "AU" },
9
+ "ap-southeast-2": { "name": "Sydney", "lat": -33.87, "lon": 151.21, "zone": "AU-NSW", "annual_zone": "AU", "jurisdiction": "AU" },
10
10
  "ca-central-1": { "name": "Montreal", "lat": 45.50, "lon": -73.57, "zone": "CA-QC", "ca_source": "hq", "jurisdiction": "CA" },
11
11
  "ap-southeast-1": { "name": "Singapore", "lat": 1.29, "lon": 103.85, "zone": "SG", "annual_zone": "SG", "jurisdiction": "SG" },
12
12
  "ap-northeast-1": { "name": "Tokyo", "lat": 35.68, "lon": 139.69, "zone": "JP-TK", "annual_zone": "JP", "jurisdiction": "JP" },
@@ -8,6 +8,17 @@ from __future__ import annotations
8
8
  from typing import Optional
9
9
 
10
10
  from mcp.server.fastmcp import FastMCP
11
+ from mcp.types import ToolAnnotations
12
+
13
+ # Tool annotations let MCP hosts decide whether to ask the user before a call (and some
14
+ # directories require them). Every GreenPick tool only reads and computes: nothing is created,
15
+ # changed or deleted, so repeating a call has no extra effect. Most fetch live grid data from
16
+ # outside services (EIA, ENTSO-E, Hydro-Québec, IESO) -> openWorldHint; the methodology tool
17
+ # returns fixed text only.
18
+ LIVE_DATA_TOOL = ToolAnnotations(readOnlyHint=True, destructiveHint=False,
19
+ idempotentHint=True, openWorldHint=True)
20
+ LOCAL_TOOL = ToolAnnotations(readOnlyHint=True, destructiveHint=False,
21
+ idempotentHint=True, openWorldHint=False)
11
22
 
12
23
  from .models import Workload, GeoPoint, Provider
13
24
  from .data import get_repository
@@ -48,7 +59,7 @@ mcp = FastMCP("greenpick", instructions=INSTRUCTIONS)
48
59
  _repo = get_repository()
49
60
 
50
61
 
51
- @mcp.tool()
62
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
52
63
  async def compare_regions_tri_axis(
53
64
  user_lat: float,
54
65
  user_lon: float,
@@ -80,7 +91,7 @@ async def compare_regions_tri_axis(
80
91
  return with_hint(rec.model_dump())
81
92
 
82
93
 
83
- @mcp.tool()
94
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
84
95
  async def greener_alternative_to(
85
96
  current_region: str,
86
97
  user_lat: float,
@@ -110,7 +121,7 @@ async def greener_alternative_to(
110
121
  return with_hint(rec.model_dump())
111
122
 
112
123
 
113
- @mcp.tool()
124
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
114
125
  async def estimate_workload_carbon(
115
126
  provider: str,
116
127
  region: str,
@@ -134,7 +145,7 @@ async def estimate_workload_carbon(
134
145
  })
135
146
 
136
147
 
137
- @mcp.tool()
148
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
138
149
  async def region_carbon_now(provider: str, region: str) -> dict:
139
150
  """Current grid carbon intensity for a region, with source, timestamp, and data freshness.
140
151
  When the reading is live, also says how it compares with what's typical for that hour this month
@@ -149,7 +160,7 @@ async def region_carbon_now(provider: str, region: str) -> dict:
149
160
  return with_hint(out)
150
161
 
151
162
 
152
- @mcp.tool()
163
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
153
164
  async def cleanest_hours_to_run(
154
165
  provider: str, region: str, top_n: int = 3, window_hours: Optional[int] = None,
155
166
  ) -> dict:
@@ -164,7 +175,7 @@ async def cleanest_hours_to_run(
164
175
  return with_hint(advice.model_dump())
165
176
 
166
177
 
167
- @mcp.tool()
178
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
168
179
  async def greener_instance_family(
169
180
  provider: str,
170
181
  region: str,
@@ -192,7 +203,7 @@ async def greener_instance_family(
192
203
  return with_hint(advice.model_dump())
193
204
 
194
205
 
195
- @mcp.tool()
206
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
196
207
  async def estimate_inference_carbon(
197
208
  size_class: str,
198
209
  monthly_requests: float,
@@ -214,7 +225,7 @@ async def estimate_inference_carbon(
214
225
  return with_hint(advice.model_dump())
215
226
 
216
227
 
217
- @mcp.tool()
228
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
218
229
  async def rightsizing_check(
219
230
  provider: str,
220
231
  region: str,
@@ -237,7 +248,7 @@ async def rightsizing_check(
237
248
  return with_hint(advice.model_dump())
238
249
 
239
250
 
240
- @mcp.tool()
251
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
241
252
  async def carbon_metric_scaffold(
242
253
  language: str = "python",
243
254
  region: Optional[str] = None,
@@ -260,7 +271,7 @@ async def carbon_metric_scaffold(
260
271
  return with_hint(scaffold.model_dump())
261
272
 
262
273
 
263
- @mcp.tool()
274
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
264
275
  async def serving_pattern_advice(
265
276
  hubs: list[dict],
266
277
  vcpu: float = 1,
@@ -290,7 +301,7 @@ async def serving_pattern_advice(
290
301
  return with_hint(advice.model_dump())
291
302
 
292
303
 
293
- @mcp.tool()
304
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
294
305
  async def data_locality_egress_check(
295
306
  compute_provider: str,
296
307
  compute_region: str,
@@ -312,7 +323,7 @@ async def data_locality_egress_check(
312
323
  return with_hint(advice.model_dump())
313
324
 
314
325
 
315
- @mcp.tool()
326
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
316
327
  async def carbon_forecast(
317
328
  provider: str, region: str, hours_ahead: int = 24, top_n: int = 5,
318
329
  ) -> dict:
@@ -327,7 +338,7 @@ async def carbon_forecast(
327
338
  return with_hint(advice.model_dump())
328
339
 
329
340
 
330
- @mcp.tool()
341
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
331
342
  async def cleanest_time_of_day(provider: str, region: str) -> dict:
332
343
  """For a RECURRING job (e.g. a nightly batch you run every day), find the region's cleanest
333
344
  time of day — each hour-of-day averaged across the recent days of data. Returns the cleanest/
@@ -337,7 +348,7 @@ async def cleanest_time_of_day(provider: str, region: str) -> dict:
337
348
  return with_hint(advice.model_dump())
338
349
 
339
350
 
340
- @mcp.tool()
351
+ @mcp.tool(annotations=LIVE_DATA_TOOL)
341
352
  async def estimate_training_carbon(
342
353
  gpu: str,
343
354
  gpu_count: int,
@@ -364,7 +375,7 @@ async def estimate_training_carbon(
364
375
  return with_hint(advice.model_dump())
365
376
 
366
377
 
367
- @mcp.tool()
378
+ @mcp.tool(annotations=LOCAL_TOOL)
368
379
  def explain_methodology_and_limits() -> dict:
369
380
  """GreenPick's assumptions and what it explicitly does NOT claim. Read this before trusting a number."""
370
381
  return {
@@ -381,6 +392,6 @@ def explain_methodology_and_limits() -> dict:
381
392
  "forecast": "Day-ahead forecast is RELATIVE: upcoming hours ranked by forecast wind+solar output (ENTSO-E A69), not an absolute gCO2/kWh forecast. EU only; US/uncovered zones degrade to the recent historical pattern (labelled 'historical').",
382
393
  "data_residency": "GreenPick flags when a recommended move CROSSES a jurisdiction (esp. leaving the EEA) as a geographic heads-up only. It does NOT assess legal/GDPR compliance, does not encode adequacy decisions, and is not legal advice — confirm with your own data governance.",
383
394
  "ignored": "Embodied/hardware carbon and provider renewable-energy certificates.",
384
- "fallback_data": "Used when live data isn't available (no key, the source didn't respond, or GreenPick isn't set to live). 'typical' = this calendar month's average intensity and average hour-of-day profile (UTC) for the region, computed from 12 months of real hourly EIA-930 / ENTSO-E data with GreenPick's own emission factors (bundled typical.json, refreshed by scripts/refresh_typical.py) — a realistic norm, not a current reading. 'annual_average' = the country's yearly lifecycle grid intensity from Ember (via Our World in Data; bundled annual.json with source and year), for regions with no free hourly data (Singapore, Tokyo, Mumbai/Pune) — sound for comparing regions, no timing signal (no hourly profile). 'mock' = a hand-set illustrative value with a synthetic hourly pattern, used only where nothing else exists (Sydney); timing advice on mock isn't meaningful. Both carry a live_data_hint saying why the result isn't live and how to turn live data on.",
395
+ "fallback_data": "Used when live data isn't available (no key, the source didn't respond, or GreenPick isn't set to live). 'typical' = this calendar month's average intensity and average hour-of-day profile (UTC) for the region, computed from 12 months of real hourly EIA-930 / ENTSO-E data with GreenPick's own emission factors (bundled typical.json, refreshed by scripts/refresh_typical.py) — a realistic norm, not a current reading. 'annual_average' = the country's yearly lifecycle grid intensity from Ember (via Our World in Data; bundled annual.json with source and year), for regions with no free hourly data (Singapore, Tokyo, Mumbai/Pune, Sydney — national figures, so Sydney's is a lower bound for its coal-heavier state) — sound for comparing regions, no timing signal (no hourly profile). 'mock' = a hand-set illustrative value with a synthetic hourly pattern, a last resort no catalog region relies on; timing advice on mock isn't meaningful. Both carry a live_data_hint saying why the result isn't live and how to turn live data on.",
385
396
  "data_sources": "GREENPICK_CARBON_SOURCE=auto routes US regions to EIA-930 and EU regions to ENTSO-E (both free, key needed), and Canadian regions to Hydro-Québec (Québec) and IESO (Ontario) open data (free, no key). All mixes are weighted with the same per-fuel factors. Without live data a region uses its clearly-labelled typical value (or mock where no free source exists). Electricity Maps is available when selected explicitly (GREENPICK_CARBON_SOURCE=electricitymaps).",
386
397
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: greenpick
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
5
5
  Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering
19
19
  Requires-Python: >=3.11
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: mcp<2,>=1.2
22
+ Requires-Dist: mcp<2,>=1.7
23
23
  Requires-Dist: pydantic>=2.0
24
24
  Requires-Dist: httpx>=0.27
25
25
  Provides-Extra: dev
@@ -60,11 +60,12 @@ seconds. (It responds when you ask; auto-nudging on infra-code changes is on the
60
60
  > this month's average for each grid, from a year of real data.
61
61
 
62
62
  <p align="center">
63
- <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~30× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
63
+ <img src="https://raw.githubusercontent.com/bithug01/greenpick/master/docs/img/region-carbon-30x.svg" alt="Same H100 training run, ~28× the carbon depending only on region (Stockholm vs Sydney)" width="560" />
64
64
  </p>
65
65
 
66
- *The same 40-GPU-hour training run emits ~0.7 kg CO₂ in Stockholm vs ~19 kg in Sydney — a ~30×
67
- difference from region choice alone. Ask GreenPick and it surfaces that (with the cost and latency
66
+ *The same 40-GPU-hour training run emits ~0.6 kg CO₂ in Stockholm vs ~17.5 kg in Sydney — a ~28×
67
+ difference from region choice alone (typical October grids; Sydney on Ember's 2025 national figure,
68
+ a lower bound for its coal-heavier state grid). Ask GreenPick and it surfaces that (with the cost and latency
68
69
  tradeoff) while you're still in the editor, before you commit.*
69
70
 
70
71
  **▶ [See the 5-minute demo](docs/demo.md)** — three scenarios with real tool output (training placement, the egress backfire, the forecast).
@@ -195,12 +196,12 @@ See `docs/user-questions-backlog.md`.
195
196
  intensity and average hour-of-day profile (UTC), computed from 12 months of real hourly EIA-930 /
196
197
  ENTSO-E data (`data/typical.json`, refreshed by `scripts/refresh_typical.py`). Labelled
197
198
  `quality: "typical"` — a realistic norm, never presented as a current reading.
198
- - **Annual** — for Singapore, Tokyo and Mumbai/Pune, where no free hourly data exists: the country's
199
+ - **Annual** — for Singapore, Tokyo, Mumbai/Pune and Sydney, where no free hourly data exists: the country's
199
200
  yearly lifecycle grid intensity from [Ember](https://ember-energy.org/) (via Our World in Data,
200
201
  CC BY 4.0), with source and year (`data/annual.json`). Labelled `quality: "annual_average"`; fine for
201
202
  comparing regions, but carries no timing signal.
202
- - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, used only where nothing else
203
- exists (currently Sydney).
203
+ - **Mock** — a hand-set illustrative value, labelled `quality: "mock"`, kept only as a last resort if
204
+ every other source is missing. No catalog region relies on it.
204
205
  - Cost: a **dated static pricing table** (`data/pricing.json`), refreshed from live Vantage
205
206
  pricing via `scripts/refresh_pricing.py` using one matched method across all three clouds.
206
207
  - Instance-family efficiency: directional perf-per-watt multipliers (`data/instance_families.json`).
@@ -318,7 +319,7 @@ carbon, set two free keys (`EIA_API_KEY` for US, `ENTSOE_TOKEN` for EU) and
318
319
  `GREENPICK_CARBON_SOURCE=auto` — see [Turn on live data](#turn-on-live-data).
319
320
 
320
321
  **Why "directional" and not audited numbers?** Because it's for a *design-time decision*, not a
321
- compliance report. "Stockholm is ~30× cleaner than Sydney" is what you need to pick a region in
322
+ compliance report. "Stockholm is ~28× cleaner than Sydney" is what you need to pick a region in
322
323
  seconds — a certified figure is a different job for a different tool (Electricity Maps, WattTime).
323
324
  GreenPick is for *decisions*, not *disclosures*, and says so in every tool's limitations.
324
325
 
@@ -1,4 +1,4 @@
1
- mcp<2,>=1.2
1
+ mcp<2,>=1.7
2
2
  pydantic>=2.0
3
3
  httpx>=0.27
4
4
 
@@ -348,10 +348,10 @@ async def test_auto_router_picks_source_by_region(monkeypatch):
348
348
  repo = AutoRepository(eia_key="k", entsoe_token="t")
349
349
  us = await repo.carbon_now("aws", "us-east-1") # -> EIA
350
350
  eu = await repo.carbon_now("aws", "eu-north-1") # -> ENTSO-E
351
- syd = await repo.carbon_now("aws", "ap-southeast-2") # -> mock
351
+ syd = await repo.carbon_now("aws", "ap-southeast-2") # -> no live source: sourced yearly figure
352
352
  assert us.source == "eia"
353
353
  assert eu.source == "entsoe"
354
- assert syd.quality == "mock"
354
+ assert syd.quality == "annual_average"
355
355
 
356
356
 
357
357
  @pytest.mark.asyncio
@@ -1494,14 +1494,26 @@ async def test_typical_reading_is_this_months_real_average():
1494
1494
 
1495
1495
 
1496
1496
  @pytest.mark.asyncio
1497
- async def test_uncovered_region_still_falls_back_to_mock():
1497
+ async def test_mock_is_only_the_last_resort(monkeypatch):
1498
+ from greenpick.data import typical_repository as tr
1498
1499
  repo = TypicalCarbonRepository()
1500
+ # Sydney has no typical data but a sourced yearly figure -> annual, not mock.
1499
1501
  r = await repo.carbon_now("aws", "ap-southeast-2")
1500
- assert r.quality == "mock"
1502
+ assert r.quality == "annual_average" and r.g_co2_per_kwh == 525.2
1503
+ # With no yearly figure either, mock still answers (labelled) rather than failing.
1504
+ monkeypatch.setattr(tr, "annual_entry", lambda p, reg: None)
1505
+ m = await repo.carbon_now("aws", "ap-southeast-2")
1506
+ assert m.quality == "mock"
1501
1507
  hist = await repo.carbon_history("aws", "ap-southeast-2")
1502
1508
  assert hist and hist[0].period.startswith("H") and not hist[0].period.endswith("Z")
1503
1509
 
1504
1510
 
1511
+ def test_no_catalog_region_relies_on_mock():
1512
+ from greenpick.data.static_data import iter_regions
1513
+ for prov, reg, meta in iter_regions():
1514
+ assert typical_entry(prov, reg) or annual_entry(prov, reg), f"{prov}/{reg} would be mock"
1515
+
1516
+
1505
1517
  @pytest.mark.asyncio
1506
1518
  async def test_typical_history_is_the_months_real_profile_in_utc():
1507
1519
  from greenpick.services import scheduling_service as ss
@@ -1780,3 +1792,55 @@ async def test_asian_users_are_now_served_from_asia():
1780
1792
  adv = await serving_service.serving_pattern(TypicalCarbonRepository(), hubs, Workload(vcpu=1, ram_gb=2))
1781
1793
  assert adv.single_region.region in ("ap-southeast-1", "asia-southeast1", "southeastasia")
1782
1794
  assert adv.single_region.weighted_latency_ms < 40 # was ~199 ms from Sydney
1795
+
1796
+
1797
+ # --- 0.4.2: tool annotations on every tool; every tool tested by name ---
1798
+
1799
+ def test_every_tool_declares_all_four_annotation_hints():
1800
+ import asyncio
1801
+ from greenpick import server
1802
+ tools = asyncio.run(server.mcp.list_tools())
1803
+ assert len(tools) == 15
1804
+ for t in tools:
1805
+ a = t.annotations
1806
+ assert a is not None, t.name
1807
+ for hint in (a.readOnlyHint, a.destructiveHint, a.idempotentHint, a.openWorldHint):
1808
+ assert isinstance(hint, bool), t.name # explicit booleans, none missing
1809
+ assert a.readOnlyHint and not a.destructiveHint and a.idempotentHint, t.name
1810
+ local = {t.name for t in tools if not t.annotations.openWorldHint}
1811
+ assert local == {"explain_methodology_and_limits"} # the only tool that fetches nothing
1812
+
1813
+
1814
+ def _tool(name):
1815
+ from greenpick import server
1816
+ f = getattr(server, name)
1817
+ return getattr(f, "fn", f)
1818
+
1819
+
1820
+ @pytest.mark.asyncio
1821
+ async def test_tool_greener_alternative_to(monkeypatch):
1822
+ from greenpick import server
1823
+ monkeypatch.setattr(server, "_repo", MockCarbonRepository())
1824
+ r = await _tool("greener_alternative_to")(current_region="us-east-1", user_lat=40.71, user_lon=-74.01,
1825
+ vcpu=4, ram_gb=16, max_added_latency_ms=50)
1826
+ assert r["recommended_region"] in ("aws/ca-central-1", "gcp/northamerica-northeast1", "azure/canadaeast")
1827
+ picked = next(c for c in r["candidates"]
1828
+ if f"{c['provider']}/{c['region']}" == r["recommended_region"])
1829
+ assert picked["added_latency_ms"] <= 50 + 15
1830
+
1831
+
1832
+ @pytest.mark.asyncio
1833
+ async def test_tool_cleanest_hours_to_run(monkeypatch):
1834
+ from greenpick import server
1835
+ monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
1836
+ r = await _tool("cleanest_hours_to_run")(provider="aws", region="us-west-2", window_hours=4)
1837
+ assert r["quality"] == "typical" and r["cleanest"]
1838
+ assert r["window"]["window_hours"] == 4 and r["window"]["start_hour_of_day"] is not None
1839
+ assert "live_data_hint" in r
1840
+
1841
+
1842
+ def test_tool_explain_methodology_and_limits():
1843
+ r = _tool("explain_methodology_and_limits")()
1844
+ for key in ("what_it_is", "what_it_is_not", "carbon", "cost", "fallback_data", "data_sources"):
1845
+ assert r.get(key), key
1846
+ assert "annual_average" in r["fallback_data"] and "Hydro-Québec" in r["data_sources"]
File without changes
File without changes