greenpick 0.4.1__tar.gz → 0.5.0__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.
- {greenpick-0.4.1/src/greenpick.egg-info → greenpick-0.5.0}/PKG-INFO +7 -5
- {greenpick-0.4.1 → greenpick-0.5.0}/README.md +5 -3
- {greenpick-0.4.1 → greenpick-0.5.0}/pyproject.toml +2 -2
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/__init__.py +1 -1
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/models.py +29 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/server.py +74 -19
- greenpick-0.5.0/src/greenpick/services/offhours_service.py +82 -0
- greenpick-0.5.0/src/greenpick/services/typical_ranking_service.py +106 -0
- {greenpick-0.4.1 → greenpick-0.5.0/src/greenpick.egg-info}/PKG-INFO +7 -5
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick.egg-info/SOURCES.txt +2 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick.egg-info/requires.txt +1 -1
- {greenpick-0.4.1 → greenpick-0.5.0}/tests/test_services.py +1884 -1794
- {greenpick-0.4.1 → greenpick-0.5.0}/LICENSE +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/setup.cfg +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/__main__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/config.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/annual.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/network.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/power.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/pricing.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/carbon_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.4.1 → greenpick-0.5.0}/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.
|
|
3
|
+
Version: 0.5.0
|
|
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.
|
|
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
|
|
@@ -44,7 +44,7 @@ Dynamic: license-file
|
|
|
44
44
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
45
45
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
46
46
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
47
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
47
|
+
<img src="https://img.shields.io/badge/MCP%20tools-17-0ea5e9" alt="MCP tools: 17" />
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -52,7 +52,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
52
52
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
53
53
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
54
54
|
|
|
55
|
-
> **Status: working —
|
|
55
|
+
> **Status: working — 17 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
56
56
|
> IESO — no key needed), cross-cloud cost
|
|
57
57
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
58
58
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -143,7 +143,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
143
143
|
|
|
144
144
|
## MCP tools
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
17 tools, grouped by what you're trying to do.
|
|
147
147
|
|
|
148
148
|
### Pick a region
|
|
149
149
|
|
|
@@ -159,6 +159,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
159
159
|
|------|--------------|
|
|
160
160
|
| `cleanest_hours_to_run` | Cleanest recent hours to time a deferrable job (ETL, backups, training), + % reduction. Pass `window_hours` for a job of known duration → cleanest contiguous window to start at. |
|
|
161
161
|
| `cleanest_time_of_day` | For a **recurring** job (nightly batch): the region's cleanest time of day, averaged across recent days. A daily pattern, not a 7-day weekly one. |
|
|
162
|
+
| `greenest_regions_on_average` | Which regions are cleanest on a **typical** day in a given month (or the whole year), ranked from last year's real data. Offline, no keys; a planning view, not today's grid. |
|
|
162
163
|
| `carbon_forecast` | Forward-looking: which **upcoming** hours will be cleanest, from the EU day-ahead wind+solar forecast (ENTSO-E). Relative ranking; US/uncovered zones degrade to the recent historical pattern. |
|
|
163
164
|
|
|
164
165
|
### Estimate a workload's carbon
|
|
@@ -175,6 +176,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
175
176
|
|------|--------------|
|
|
176
177
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
177
178
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
179
|
+
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
178
180
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
179
181
|
| `serving_pattern_advice` | For a user-facing platform: serve from one region or several? Traffic-weighted latency + carbon, with the multi-region data-sync egress and cross-jurisdiction residency catches. |
|
|
180
182
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
15
15
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
16
16
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
17
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
17
|
+
<img src="https://img.shields.io/badge/MCP%20tools-17-0ea5e9" alt="MCP tools: 17" />
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -22,7 +22,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
22
22
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
23
23
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
24
24
|
|
|
25
|
-
> **Status: working —
|
|
25
|
+
> **Status: working — 17 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
26
26
|
> IESO — no key needed), cross-cloud cost
|
|
27
27
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
28
28
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -113,7 +113,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
113
113
|
|
|
114
114
|
## MCP tools
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
17 tools, grouped by what you're trying to do.
|
|
117
117
|
|
|
118
118
|
### Pick a region
|
|
119
119
|
|
|
@@ -129,6 +129,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
129
129
|
|------|--------------|
|
|
130
130
|
| `cleanest_hours_to_run` | Cleanest recent hours to time a deferrable job (ETL, backups, training), + % reduction. Pass `window_hours` for a job of known duration → cleanest contiguous window to start at. |
|
|
131
131
|
| `cleanest_time_of_day` | For a **recurring** job (nightly batch): the region's cleanest time of day, averaged across recent days. A daily pattern, not a 7-day weekly one. |
|
|
132
|
+
| `greenest_regions_on_average` | Which regions are cleanest on a **typical** day in a given month (or the whole year), ranked from last year's real data. Offline, no keys; a planning view, not today's grid. |
|
|
132
133
|
| `carbon_forecast` | Forward-looking: which **upcoming** hours will be cleanest, from the EU day-ahead wind+solar forecast (ENTSO-E). Relative ranking; US/uncovered zones degrade to the recent historical pattern. |
|
|
133
134
|
|
|
134
135
|
### Estimate a workload's carbon
|
|
@@ -145,6 +146,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
145
146
|
|------|--------------|
|
|
146
147
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
147
148
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
149
|
+
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
148
150
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
149
151
|
| `serving_pattern_advice` | For a user-facing platform: serve from one region or several? Traffic-weighted latency + carbon, with the multi-region data-sync egress and cross-jurisdiction residency catches. |
|
|
150
152
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
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.
|
|
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.
|
|
7
|
+
__version__ = "0.5.0"
|
|
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.
|
|
@@ -348,3 +348,32 @@ class Recommendation(BaseModel):
|
|
|
348
348
|
residency_note: Optional[str] = None # neutral heads-up if the pick crosses jurisdictions
|
|
349
349
|
rationale: str = ""
|
|
350
350
|
limitations: str = ""
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class OffHoursAdvice(BaseModel):
|
|
354
|
+
"""What switching a non-prod / idle environment off outside the hours it's needed saves.
|
|
355
|
+
|
|
356
|
+
The other FinOps basic next to right-sizing: the cheapest hour is the one you don't run.
|
|
357
|
+
Cost and carbon both scale with running hours, so the saving is the same fraction of each.
|
|
358
|
+
Directional, compute only."""
|
|
359
|
+
provider: Provider
|
|
360
|
+
region: str
|
|
361
|
+
vcpu: float
|
|
362
|
+
ram_gb: float
|
|
363
|
+
hours_per_week_on: float
|
|
364
|
+
hours_per_month_now: float # always-on baseline (730)
|
|
365
|
+
hours_per_month_scheduled: float
|
|
366
|
+
on_share_pct: float # share of the month it's running
|
|
367
|
+
cost_now_usd_per_month: Optional[float] = None
|
|
368
|
+
cost_scheduled_usd_per_month: Optional[float] = None
|
|
369
|
+
monthly_usd_saving: Optional[float] = None
|
|
370
|
+
cost_saving_pct: Optional[float] = None
|
|
371
|
+
carbon_now_kg_per_month: Optional[float] = None
|
|
372
|
+
carbon_scheduled_kg_per_month: Optional[float] = None
|
|
373
|
+
monthly_kg_co2_saving: Optional[float] = None
|
|
374
|
+
carbon_saving_pct: Optional[float] = None
|
|
375
|
+
is_worthwhile: bool = False
|
|
376
|
+
carbon_source: Optional[str] = None
|
|
377
|
+
carbon_quality: Optional[str] = None
|
|
378
|
+
rationale: str = ""
|
|
379
|
+
limitations: str = ""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""MCP server: exposes GreenPick's
|
|
1
|
+
"""MCP server: exposes GreenPick's 17 tools over FastMCP.
|
|
2
2
|
|
|
3
3
|
Thin layer — parse/validate inputs, call services, return JSON-friendly dicts. All math
|
|
4
4
|
lives in services/; this file only wires tools to them.
|
|
@@ -8,13 +8,24 @@ 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
|
|
14
25
|
from .services import (
|
|
15
26
|
carbon_service, recommender, scheduling_service, instance_service, locality_service,
|
|
16
27
|
training_service, forecast_service, serving_service, metric_scaffold_service,
|
|
17
|
-
rightsizing_service, inference_service,
|
|
28
|
+
rightsizing_service, inference_service, offhours_service, typical_ranking_service,
|
|
18
29
|
)
|
|
19
30
|
from .models import ServingHub
|
|
20
31
|
from .services.data_hint import with_hint
|
|
@@ -27,13 +38,15 @@ INSTRUCTIONS = """\
|
|
|
27
38
|
GreenPick advises on cloud infrastructure decisions BEFORE you deploy, weighing carbon, cost and \
|
|
28
39
|
latency together. Strongest uses, in order:
|
|
29
40
|
1. Right-sizing (rightsizing_check): an over-provisioned instance cut to a standard, buyable size \
|
|
30
|
-
saves money and carbon in the same move. Lead with the monthly dollar saving.
|
|
41
|
+
saves money and carbon in the same move. Lead with the monthly dollar saving. For dev, test or other \
|
|
42
|
+
environments that don't need to run 24/7, offhours_check shows what switching them off saves.
|
|
31
43
|
2. Scheduling deferrable jobs: cleanest_hours_to_run (one-off), cleanest_time_of_day (recurring), \
|
|
32
44
|
carbon_forecast (EU day-ahead).
|
|
33
45
|
3. GPU training and LLM inference footprints: estimate_training_carbon (gpu_hours is the TOTAL across \
|
|
34
46
|
all GPUs; report wall_clock_hours as the run length), estimate_inference_carbon.
|
|
35
47
|
4. Efficiency and placement: greener_instance_family, compare_regions_tri_axis, greener_alternative_to, \
|
|
36
|
-
serving_pattern_advice
|
|
48
|
+
serving_pattern_advice, greenest_regions_on_average (which regions are normally cleanest in a month); \
|
|
49
|
+
carbon_metric_scaffold for an app to track its own estimated carbon.
|
|
37
50
|
Before recommending a region move, run data_locality_egress_check: egress can erase the win. Mention \
|
|
38
51
|
any residency_note.
|
|
39
52
|
Every carbon figure carries source and quality fields. Report 'typical', 'mock', 'cached' or \
|
|
@@ -48,7 +61,7 @@ mcp = FastMCP("greenpick", instructions=INSTRUCTIONS)
|
|
|
48
61
|
_repo = get_repository()
|
|
49
62
|
|
|
50
63
|
|
|
51
|
-
@mcp.tool()
|
|
64
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
52
65
|
async def compare_regions_tri_axis(
|
|
53
66
|
user_lat: float,
|
|
54
67
|
user_lon: float,
|
|
@@ -80,7 +93,7 @@ async def compare_regions_tri_axis(
|
|
|
80
93
|
return with_hint(rec.model_dump())
|
|
81
94
|
|
|
82
95
|
|
|
83
|
-
@mcp.tool()
|
|
96
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
84
97
|
async def greener_alternative_to(
|
|
85
98
|
current_region: str,
|
|
86
99
|
user_lat: float,
|
|
@@ -110,7 +123,7 @@ async def greener_alternative_to(
|
|
|
110
123
|
return with_hint(rec.model_dump())
|
|
111
124
|
|
|
112
125
|
|
|
113
|
-
@mcp.tool()
|
|
126
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
114
127
|
async def estimate_workload_carbon(
|
|
115
128
|
provider: str,
|
|
116
129
|
region: str,
|
|
@@ -134,7 +147,7 @@ async def estimate_workload_carbon(
|
|
|
134
147
|
})
|
|
135
148
|
|
|
136
149
|
|
|
137
|
-
@mcp.tool()
|
|
150
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
138
151
|
async def region_carbon_now(provider: str, region: str) -> dict:
|
|
139
152
|
"""Current grid carbon intensity for a region, with source, timestamp, and data freshness.
|
|
140
153
|
When the reading is live, also says how it compares with what's typical for that hour this month
|
|
@@ -149,7 +162,7 @@ async def region_carbon_now(provider: str, region: str) -> dict:
|
|
|
149
162
|
return with_hint(out)
|
|
150
163
|
|
|
151
164
|
|
|
152
|
-
@mcp.tool()
|
|
165
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
153
166
|
async def cleanest_hours_to_run(
|
|
154
167
|
provider: str, region: str, top_n: int = 3, window_hours: Optional[int] = None,
|
|
155
168
|
) -> dict:
|
|
@@ -164,7 +177,7 @@ async def cleanest_hours_to_run(
|
|
|
164
177
|
return with_hint(advice.model_dump())
|
|
165
178
|
|
|
166
179
|
|
|
167
|
-
@mcp.tool()
|
|
180
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
168
181
|
async def greener_instance_family(
|
|
169
182
|
provider: str,
|
|
170
183
|
region: str,
|
|
@@ -192,7 +205,7 @@ async def greener_instance_family(
|
|
|
192
205
|
return with_hint(advice.model_dump())
|
|
193
206
|
|
|
194
207
|
|
|
195
|
-
@mcp.tool()
|
|
208
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
196
209
|
async def estimate_inference_carbon(
|
|
197
210
|
size_class: str,
|
|
198
211
|
monthly_requests: float,
|
|
@@ -214,7 +227,7 @@ async def estimate_inference_carbon(
|
|
|
214
227
|
return with_hint(advice.model_dump())
|
|
215
228
|
|
|
216
229
|
|
|
217
|
-
@mcp.tool()
|
|
230
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
218
231
|
async def rightsizing_check(
|
|
219
232
|
provider: str,
|
|
220
233
|
region: str,
|
|
@@ -237,7 +250,49 @@ async def rightsizing_check(
|
|
|
237
250
|
return with_hint(advice.model_dump())
|
|
238
251
|
|
|
239
252
|
|
|
240
|
-
@mcp.tool()
|
|
253
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
254
|
+
async def offhours_check(
|
|
255
|
+
provider: str,
|
|
256
|
+
region: str,
|
|
257
|
+
vcpu: float,
|
|
258
|
+
ram_gb: float,
|
|
259
|
+
hours_per_week_on: float,
|
|
260
|
+
) -> dict:
|
|
261
|
+
"""Non-prod / idle scheduling, the other FinOps basic next to right-sizing: "this doesn't need to
|
|
262
|
+
run 24/7." Give the instance size and how many hours a WEEK it actually needs to be on (weekdays
|
|
263
|
+
9-17 is 40, weekdays 7-19 is 60, around the clock is 168). Returns the monthly $ AND kg CO2 saved
|
|
264
|
+
by switching it off the rest of the time. Lead with the dollar number. Only for on-demand capacity
|
|
265
|
+
that tolerates being off (dev, test, staging, demo, batch); reserved instances and savings plans
|
|
266
|
+
are paid for regardless. Directional, compute only."""
|
|
267
|
+
if not 0 < hours_per_week_on <= 168:
|
|
268
|
+
return {"provider": provider, "region": region,
|
|
269
|
+
"error": "hours_per_week_on must be between 0 and 168 (there are 168 hours in a week)."}
|
|
270
|
+
advice = await offhours_service.offhours_check(
|
|
271
|
+
_repo, provider, region, vcpu, ram_gb, hours_per_week_on, # type: ignore[arg-type]
|
|
272
|
+
)
|
|
273
|
+
return with_hint(advice.model_dump())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@mcp.tool(annotations=LOCAL_TOOL)
|
|
277
|
+
def greenest_regions_on_average(
|
|
278
|
+
month: Optional[int] = None,
|
|
279
|
+
whole_year: bool = False,
|
|
280
|
+
providers: Optional[list[str]] = None,
|
|
281
|
+
regions: Optional[list[str]] = None,
|
|
282
|
+
top_n: Optional[int] = None,
|
|
283
|
+
) -> dict:
|
|
284
|
+
"""Which regions are normally the cleanest in a given month (default: the current one), or over
|
|
285
|
+
the whole year? Ranks regions by their TYPICAL average grid carbon, built from a year of real
|
|
286
|
+
hourly data, with each region's cleanest and dirtiest month. Use it to pick a region on a monthly
|
|
287
|
+
norm rather than a single hour; use region_carbon_now for what the grid is doing today. Carbon only
|
|
288
|
+
(no cost or latency; compare_regions_tri_axis weighs all three). Offline, needs no keys. Regions
|
|
289
|
+
labelled annual_average have only a yearly national figure and are not month-specific."""
|
|
290
|
+
return typical_ranking_service.rank_regions(
|
|
291
|
+
month=month, whole_year=whole_year, providers=providers, regions=regions, top_n=top_n,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
241
296
|
async def carbon_metric_scaffold(
|
|
242
297
|
language: str = "python",
|
|
243
298
|
region: Optional[str] = None,
|
|
@@ -260,7 +315,7 @@ async def carbon_metric_scaffold(
|
|
|
260
315
|
return with_hint(scaffold.model_dump())
|
|
261
316
|
|
|
262
317
|
|
|
263
|
-
@mcp.tool()
|
|
318
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
264
319
|
async def serving_pattern_advice(
|
|
265
320
|
hubs: list[dict],
|
|
266
321
|
vcpu: float = 1,
|
|
@@ -290,7 +345,7 @@ async def serving_pattern_advice(
|
|
|
290
345
|
return with_hint(advice.model_dump())
|
|
291
346
|
|
|
292
347
|
|
|
293
|
-
@mcp.tool()
|
|
348
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
294
349
|
async def data_locality_egress_check(
|
|
295
350
|
compute_provider: str,
|
|
296
351
|
compute_region: str,
|
|
@@ -312,7 +367,7 @@ async def data_locality_egress_check(
|
|
|
312
367
|
return with_hint(advice.model_dump())
|
|
313
368
|
|
|
314
369
|
|
|
315
|
-
@mcp.tool()
|
|
370
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
316
371
|
async def carbon_forecast(
|
|
317
372
|
provider: str, region: str, hours_ahead: int = 24, top_n: int = 5,
|
|
318
373
|
) -> dict:
|
|
@@ -327,7 +382,7 @@ async def carbon_forecast(
|
|
|
327
382
|
return with_hint(advice.model_dump())
|
|
328
383
|
|
|
329
384
|
|
|
330
|
-
@mcp.tool()
|
|
385
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
331
386
|
async def cleanest_time_of_day(provider: str, region: str) -> dict:
|
|
332
387
|
"""For a RECURRING job (e.g. a nightly batch you run every day), find the region's cleanest
|
|
333
388
|
time of day — each hour-of-day averaged across the recent days of data. Returns the cleanest/
|
|
@@ -337,7 +392,7 @@ async def cleanest_time_of_day(provider: str, region: str) -> dict:
|
|
|
337
392
|
return with_hint(advice.model_dump())
|
|
338
393
|
|
|
339
394
|
|
|
340
|
-
@mcp.tool()
|
|
395
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
341
396
|
async def estimate_training_carbon(
|
|
342
397
|
gpu: str,
|
|
343
398
|
gpu_count: int,
|
|
@@ -364,7 +419,7 @@ async def estimate_training_carbon(
|
|
|
364
419
|
return with_hint(advice.model_dump())
|
|
365
420
|
|
|
366
421
|
|
|
367
|
-
@mcp.tool()
|
|
422
|
+
@mcp.tool(annotations=LOCAL_TOOL)
|
|
368
423
|
def explain_methodology_and_limits() -> dict:
|
|
369
424
|
"""GreenPick's assumptions and what it explicitly does NOT claim. Read this before trusting a number."""
|
|
370
425
|
return {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Off-hours scheduling: "this doesn't need to run 24/7."
|
|
2
|
+
|
|
3
|
+
Dev, test, staging, demo and batch environments are often left running all night and all weekend.
|
|
4
|
+
Switching them off when nobody uses them is the other FinOps basic next to right-sizing, and it has
|
|
5
|
+
the same dual benefit: cost and carbon both scale with running hours, so every hour you don't run
|
|
6
|
+
saves both, with no tradeoff in the hours you do.
|
|
7
|
+
|
|
8
|
+
Design-time and directional. It reuses GreenPick's cost table and power model unchanged: the only
|
|
9
|
+
new input is how many hours a week the environment actually needs to be on.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ..data.repository import CarbonRepository
|
|
14
|
+
from ..models import OffHoursAdvice, Provider, Workload
|
|
15
|
+
from . import carbon_service, cost_service
|
|
16
|
+
|
|
17
|
+
HOURS_PER_WEEK_ALWAYS_ON = 168
|
|
18
|
+
HOURS_PER_MONTH_ALWAYS_ON = 730 # GreenPick's always-on baseline
|
|
19
|
+
WEEKS_PER_MONTH = HOURS_PER_MONTH_ALWAYS_ON / HOURS_PER_WEEK_ALWAYS_ON # ~4.345
|
|
20
|
+
_MIN_SAVING_PCT = 10.0 # below this, not worth the machinery
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def offhours_check(
|
|
24
|
+
repo: CarbonRepository,
|
|
25
|
+
provider: Provider,
|
|
26
|
+
region: str,
|
|
27
|
+
vcpu: float,
|
|
28
|
+
ram_gb: float,
|
|
29
|
+
hours_per_week_on: float,
|
|
30
|
+
) -> OffHoursAdvice:
|
|
31
|
+
scheduled_hours = round(min(HOURS_PER_MONTH_ALWAYS_ON, hours_per_week_on * WEEKS_PER_MONTH), 1)
|
|
32
|
+
now_wl = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=HOURS_PER_MONTH_ALWAYS_ON)
|
|
33
|
+
sched_wl = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=scheduled_hours)
|
|
34
|
+
|
|
35
|
+
reading = await carbon_service.carbon_now(repo, provider, region)
|
|
36
|
+
cost_now = cost_service.estimate_monthly_cost_usd(provider, now_wl)
|
|
37
|
+
cost_s = cost_service.estimate_monthly_cost_usd(provider, sched_wl)
|
|
38
|
+
carbon_now = carbon_service.estimate_kg_co2_per_month(now_wl, reading)
|
|
39
|
+
carbon_s = carbon_service.estimate_kg_co2_per_month(sched_wl, reading)
|
|
40
|
+
|
|
41
|
+
usd_saving = round(cost_now - cost_s, 2) if cost_now is not None and cost_s is not None else None
|
|
42
|
+
cost_pct = round((cost_now - cost_s) / cost_now * 100, 1) if cost_now and cost_s is not None else None
|
|
43
|
+
kg_saving = round(carbon_now - carbon_s, 2)
|
|
44
|
+
carbon_pct = round((carbon_now - carbon_s) / carbon_now * 100, 1) if carbon_now > 0 else None
|
|
45
|
+
on_share = round(scheduled_hours / HOURS_PER_MONTH_ALWAYS_ON * 100, 1)
|
|
46
|
+
worthwhile = bool(carbon_pct is not None and carbon_pct >= _MIN_SAVING_PCT)
|
|
47
|
+
|
|
48
|
+
if worthwhile:
|
|
49
|
+
usd_txt = f"~${usd_saving:,.0f}/mo" if usd_saving is not None else "a lower bill"
|
|
50
|
+
rationale = (
|
|
51
|
+
f"Running ~{hours_per_week_on:g} h a week instead of around the clock (168 h) means it is on "
|
|
52
|
+
f"{on_share:.0f}% of the time: ~{cost_pct:.0f}% cheaper ({usd_txt} saved) AND "
|
|
53
|
+
f"~{carbon_pct:.0f}% less carbon (~{kg_saving:.1f} kg CO2/mo, {reading.quality} grid data). "
|
|
54
|
+
f"Same hours of use, nothing running when nobody is there. Only worth it for an "
|
|
55
|
+
f"environment that can be off, and you need something to stop and start it on schedule.")
|
|
56
|
+
else:
|
|
57
|
+
rationale = (
|
|
58
|
+
f"At ~{hours_per_week_on:g} h a week it is already on {on_share:.0f}% of the time, so "
|
|
59
|
+
f"switching it off outside those hours would save under {_MIN_SAVING_PCT:.0f}% — not worth "
|
|
60
|
+
f"building a scheduler for. Look to right-sizing or region choice instead.")
|
|
61
|
+
|
|
62
|
+
return OffHoursAdvice(
|
|
63
|
+
provider=provider, region=region, vcpu=vcpu, ram_gb=ram_gb,
|
|
64
|
+
hours_per_week_on=hours_per_week_on,
|
|
65
|
+
hours_per_month_now=HOURS_PER_MONTH_ALWAYS_ON, hours_per_month_scheduled=scheduled_hours,
|
|
66
|
+
on_share_pct=on_share,
|
|
67
|
+
cost_now_usd_per_month=cost_now, cost_scheduled_usd_per_month=cost_s,
|
|
68
|
+
monthly_usd_saving=usd_saving, cost_saving_pct=cost_pct,
|
|
69
|
+
carbon_now_kg_per_month=carbon_now, carbon_scheduled_kg_per_month=carbon_s,
|
|
70
|
+
monthly_kg_co2_saving=kg_saving, carbon_saving_pct=carbon_pct,
|
|
71
|
+
is_worthwhile=worthwhile,
|
|
72
|
+
carbon_source=reading.source, carbon_quality=reading.quality,
|
|
73
|
+
rationale=rationale,
|
|
74
|
+
limitations=(
|
|
75
|
+
"Directional, compute only. A stopped instance still costs for its attached disks, IP "
|
|
76
|
+
"addresses and snapshots, so the real bill falls a little less than shown. Reserved "
|
|
77
|
+
"instances and savings plans are paid for whether the instance runs or not, so they cancel "
|
|
78
|
+
"the saving: this applies to on-demand capacity. Assumes the instance works as hard when it "
|
|
79
|
+
"is on, and that start-up time and data warm-up are acceptable. Carbon uses today's grid "
|
|
80
|
+
"intensity; the hours you actually run may be cleaner or dirtier (see cleanest_time_of_day). "
|
|
81
|
+
"Hours per week: weekdays 9-17 is 40, weekdays 7-19 is 60, around the clock is 168."),
|
|
82
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Which regions are greenest ON AVERAGE for a month (or the whole year)?
|
|
2
|
+
|
|
3
|
+
Answers the "across a whole month, which region is greenest on average?" question from the data
|
|
4
|
+
GreenPick already bundles: typical.json (12 months of real hourly EIA / ENTSO-E / Hydro-Québec /
|
|
5
|
+
IESO data, averaged per calendar month) and annual.json (a sourced yearly national figure, for
|
|
6
|
+
regions with no free hourly data).
|
|
7
|
+
|
|
8
|
+
Pure and offline: no network, no keys. Because it is built from LAST year's data it describes what
|
|
9
|
+
is typical for the time of year, not what the grid is doing today (use region_carbon_now or
|
|
10
|
+
compare_regions_tri_axis for that).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import calendar
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from ..data.static_data import iter_regions
|
|
19
|
+
from ..data.typical_repository import annual_entry, typical_entry
|
|
20
|
+
|
|
21
|
+
LIMITATIONS = (
|
|
22
|
+
"Typical figures are built from the last 12 months of real hourly grid data, averaged per "
|
|
23
|
+
"calendar month. They describe what is normal for the time of year, not today's grid, and "
|
|
24
|
+
"a given year can differ (wind, hydro, outages). Regions marked annual_average have only a "
|
|
25
|
+
"yearly national figure, so they are not month-specific: compare them with the monthly "
|
|
26
|
+
"figures with care. Carbon only; this ranking ignores cost and latency (use "
|
|
27
|
+
"compare_regions_tri_axis for the three together). Directional, not audited."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _month_name(m: int) -> str:
|
|
32
|
+
return calendar.month_name[m]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rank_regions(
|
|
36
|
+
month: Optional[int] = None,
|
|
37
|
+
whole_year: bool = False,
|
|
38
|
+
providers: Optional[list[str]] = None,
|
|
39
|
+
regions: Optional[list[str]] = None,
|
|
40
|
+
top_n: Optional[int] = None,
|
|
41
|
+
) -> dict:
|
|
42
|
+
now_month = datetime.now(timezone.utc).month
|
|
43
|
+
m = month or now_month
|
|
44
|
+
if not whole_year and not 1 <= m <= 12:
|
|
45
|
+
return {"error": f"month must be 1-12, got {month}.", "ranking": []}
|
|
46
|
+
|
|
47
|
+
rows = []
|
|
48
|
+
for prov, reg, meta in iter_regions():
|
|
49
|
+
if providers and prov not in providers:
|
|
50
|
+
continue
|
|
51
|
+
if regions and reg not in regions:
|
|
52
|
+
continue
|
|
53
|
+
entry = typical_entry(prov, reg)
|
|
54
|
+
if entry and entry.get("months"):
|
|
55
|
+
months = {int(k): v for k, v in entry["months"].items()}
|
|
56
|
+
if whole_year:
|
|
57
|
+
hours = sum(v["hours"] for v in months.values())
|
|
58
|
+
value = sum(v["avg_g_co2_per_kwh"] * v["hours"] for v in months.values()) / hours
|
|
59
|
+
elif m in months:
|
|
60
|
+
value = months[m]["avg_g_co2_per_kwh"]
|
|
61
|
+
else:
|
|
62
|
+
continue
|
|
63
|
+
avgs = {k: v["avg_g_co2_per_kwh"] for k, v in months.items()}
|
|
64
|
+
best, worst = min(avgs, key=avgs.get), max(avgs, key=avgs.get)
|
|
65
|
+
rows.append({
|
|
66
|
+
"provider": prov, "region": reg, "name": meta.get("name"),
|
|
67
|
+
"g_co2_per_kwh": round(value, 1), "quality": "typical",
|
|
68
|
+
"source": entry["source"], "data_from": entry["from"][:7], "data_to": entry["to"][:7],
|
|
69
|
+
"year_low_g_co2_per_kwh": avgs[best], "year_high_g_co2_per_kwh": avgs[worst],
|
|
70
|
+
"cleanest_month": _month_name(best), "dirtiest_month": _month_name(worst),
|
|
71
|
+
})
|
|
72
|
+
continue
|
|
73
|
+
yearly = annual_entry(prov, reg)
|
|
74
|
+
if yearly:
|
|
75
|
+
rows.append({
|
|
76
|
+
"provider": prov, "region": reg, "name": meta.get("name"),
|
|
77
|
+
"g_co2_per_kwh": yearly["g_co2_per_kwh"], "quality": "annual_average",
|
|
78
|
+
"source": "ember_annual", "data_from": str(yearly["year"]), "data_to": str(yearly["year"]),
|
|
79
|
+
"year_low_g_co2_per_kwh": None, "year_high_g_co2_per_kwh": None,
|
|
80
|
+
"cleanest_month": None, "dirtiest_month": None,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if not rows:
|
|
84
|
+
return {"error": "No regions matched.", "ranking": []}
|
|
85
|
+
|
|
86
|
+
rows.sort(key=lambda r: r["g_co2_per_kwh"])
|
|
87
|
+
worst = rows[-1]["g_co2_per_kwh"]
|
|
88
|
+
for i, r in enumerate(rows, 1):
|
|
89
|
+
r["rank"] = i
|
|
90
|
+
r["vs_dirtiest_pct"] = round((r["g_co2_per_kwh"] - worst) / worst * 100, 1) if worst else None
|
|
91
|
+
shown = rows[:top_n] if top_n else rows
|
|
92
|
+
|
|
93
|
+
period = "the whole year" if whole_year else _month_name(m)
|
|
94
|
+
best_r, worst_r = rows[0], rows[-1]
|
|
95
|
+
ratio = worst_r["g_co2_per_kwh"] / best_r["g_co2_per_kwh"] if best_r["g_co2_per_kwh"] else None
|
|
96
|
+
spread = f" (~{ratio:.0f}x apart)" if ratio and ratio >= 2 else ""
|
|
97
|
+
rationale = (
|
|
98
|
+
f"On a typical {period if whole_year else period + ' day'}, the greenest of {len(rows)} regions is "
|
|
99
|
+
f"{best_r['provider']}/{best_r['region']} at ~{best_r['g_co2_per_kwh']:.0f} gCO2/kWh and the "
|
|
100
|
+
f"dirtiest is {worst_r['provider']}/{worst_r['region']} at ~{worst_r['g_co2_per_kwh']:.0f}"
|
|
101
|
+
f"{spread}. Built from last year's real data, so it shows what is normal for the time of "
|
|
102
|
+
f"year, not what the grid is doing right now.")
|
|
103
|
+
return {
|
|
104
|
+
"period": period, "regions_ranked": len(rows), "ranking": shown,
|
|
105
|
+
"rationale": rationale, "limitations": LIMITATIONS,
|
|
106
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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.
|
|
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
|
|
@@ -44,7 +44,7 @@ Dynamic: license-file
|
|
|
44
44
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
45
45
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
46
46
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
47
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
47
|
+
<img src="https://img.shields.io/badge/MCP%20tools-17-0ea5e9" alt="MCP tools: 17" />
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -52,7 +52,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
52
52
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
53
53
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
54
54
|
|
|
55
|
-
> **Status: working —
|
|
55
|
+
> **Status: working — 17 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
56
56
|
> IESO — no key needed), cross-cloud cost
|
|
57
57
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
58
58
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -143,7 +143,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
143
143
|
|
|
144
144
|
## MCP tools
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
17 tools, grouped by what you're trying to do.
|
|
147
147
|
|
|
148
148
|
### Pick a region
|
|
149
149
|
|
|
@@ -159,6 +159,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
159
159
|
|------|--------------|
|
|
160
160
|
| `cleanest_hours_to_run` | Cleanest recent hours to time a deferrable job (ETL, backups, training), + % reduction. Pass `window_hours` for a job of known duration → cleanest contiguous window to start at. |
|
|
161
161
|
| `cleanest_time_of_day` | For a **recurring** job (nightly batch): the region's cleanest time of day, averaged across recent days. A daily pattern, not a 7-day weekly one. |
|
|
162
|
+
| `greenest_regions_on_average` | Which regions are cleanest on a **typical** day in a given month (or the whole year), ranked from last year's real data. Offline, no keys; a planning view, not today's grid. |
|
|
162
163
|
| `carbon_forecast` | Forward-looking: which **upcoming** hours will be cleanest, from the EU day-ahead wind+solar forecast (ENTSO-E). Relative ranking; US/uncovered zones degrade to the recent historical pattern. |
|
|
163
164
|
|
|
164
165
|
### Estimate a workload's carbon
|
|
@@ -175,6 +176,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
175
176
|
|------|--------------|
|
|
176
177
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
177
178
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
179
|
+
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
178
180
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
179
181
|
| `serving_pattern_advice` | For a user-facing platform: serve from one region or several? Traffic-weighted latency + carbon, with the multi-region data-sync egress and cross-jurisdiction residency catches. |
|
|
180
182
|
|
|
@@ -43,10 +43,12 @@ src/greenpick/services/instance_service.py
|
|
|
43
43
|
src/greenpick/services/latency_service.py
|
|
44
44
|
src/greenpick/services/locality_service.py
|
|
45
45
|
src/greenpick/services/metric_scaffold_service.py
|
|
46
|
+
src/greenpick/services/offhours_service.py
|
|
46
47
|
src/greenpick/services/recommender.py
|
|
47
48
|
src/greenpick/services/residency_service.py
|
|
48
49
|
src/greenpick/services/rightsizing_service.py
|
|
49
50
|
src/greenpick/services/scheduling_service.py
|
|
50
51
|
src/greenpick/services/serving_service.py
|
|
51
52
|
src/greenpick/services/training_service.py
|
|
53
|
+
src/greenpick/services/typical_ranking_service.py
|
|
52
54
|
tests/test_services.py
|