greenpick 0.4.1__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.1/src/greenpick.egg-info → greenpick-0.4.2}/PKG-INFO +2 -2
  2. {greenpick-0.4.1 → greenpick-0.4.2}/pyproject.toml +2 -2
  3. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/__init__.py +1 -1
  4. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/server.py +26 -15
  5. {greenpick-0.4.1 → greenpick-0.4.2/src/greenpick.egg-info}/PKG-INFO +2 -2
  6. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/requires.txt +1 -1
  7. {greenpick-0.4.1 → greenpick-0.4.2}/tests/test_services.py +52 -0
  8. {greenpick-0.4.1 → greenpick-0.4.2}/LICENSE +0 -0
  9. {greenpick-0.4.1 → greenpick-0.4.2}/README.md +0 -0
  10. {greenpick-0.4.1 → greenpick-0.4.2}/setup.cfg +0 -0
  11. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/__main__.py +0 -0
  12. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/config.py +0 -0
  13. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/__init__.py +0 -0
  14. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/_net.py +0 -0
  15. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/annual.json +0 -0
  16. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/auto_repository.py +0 -0
  17. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/canada_repository.py +0 -0
  18. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/eia_repository.py +0 -0
  19. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/electricitymaps_repository.py +0 -0
  20. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/emission_factors.json +0 -0
  21. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/entsoe_repository.py +0 -0
  22. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/gpu_power.json +0 -0
  23. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/inference_energy.json +0 -0
  24. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/instance_families.json +0 -0
  25. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/mock_repository.py +0 -0
  26. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/network.json +0 -0
  27. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/power.json +0 -0
  28. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/pricing.json +0 -0
  29. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/regions.json +0 -0
  30. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/repository.py +0 -0
  31. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/static_data.py +0 -0
  32. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/typical.json +0 -0
  33. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/data/typical_repository.py +0 -0
  34. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/models.py +0 -0
  35. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/__init__.py +0 -0
  36. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/carbon_service.py +0 -0
  37. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/cost_service.py +0 -0
  38. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/data_hint.py +0 -0
  39. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/forecast_service.py +0 -0
  40. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/inference_service.py +0 -0
  41. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/instance_service.py +0 -0
  42. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/latency_service.py +0 -0
  43. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/locality_service.py +0 -0
  44. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/metric_scaffold_service.py +0 -0
  45. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/recommender.py +0 -0
  46. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/residency_service.py +0 -0
  47. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/rightsizing_service.py +0 -0
  48. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/scheduling_service.py +0 -0
  49. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/serving_service.py +0 -0
  50. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick/services/training_service.py +0 -0
  51. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/SOURCES.txt +0 -0
  52. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/dependency_links.txt +0 -0
  53. {greenpick-0.4.1 → greenpick-0.4.2}/src/greenpick.egg-info/entry_points.txt +0 -0
  54. {greenpick-0.4.1 → 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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "greenpick"
3
- version = "0.4.1"
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.1"
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,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 {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: greenpick
3
- Version: 0.4.1
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
@@ -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
 
@@ -1792,3 +1792,55 @@ async def test_asian_users_are_now_served_from_asia():
1792
1792
  adv = await serving_service.serving_pattern(TypicalCarbonRepository(), hubs, Workload(vcpu=1, ram_gb=2))
1793
1793
  assert adv.single_region.region in ("ap-southeast-1", "asia-southeast1", "southeastasia")
1794
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
File without changes