weather-agent-mcp 0.1.4 → 0.1.5

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.
package/.env.example CHANGED
@@ -9,6 +9,7 @@ ANTHROPIC_MODEL=claude-3-5-haiku-latest
9
9
 
10
10
  # Open-Meteo base URLs (override in tests with fake server)
11
11
  OPEN_METEO_FORECAST_URL=https://api.open-meteo.com
12
+ OPEN_METEO_ARCHIVE_URL=https://archive-api.open-meteo.com
12
13
  OPEN_METEO_GEOCODING_URL=https://geocoding-api.open-meteo.com
13
14
 
14
15
  # Agent defaults
package/README.md CHANGED
@@ -58,7 +58,7 @@ sudo launchctl config user path "$HOME/.volta/bin:/usr/bin:/bin:/usr/sbin:/sbin"
58
58
 
59
59
  Без `sudo`: можно `launchctl setenv PATH "$HOME/.volta/bin:/usr/bin:/bin:/usr/sbin:/sbin"` и перезапустить Cursor (сбросится после reboot).
60
60
 
61
- Reload MCP. Tools: `geocode`, `getCurrentWeather`, `getForecast`, `compareWeather`.
61
+ Reload MCP. Tools: `geocode`, `getCurrentWeather`, `getForecast`, `getHistoricalWeather`, `compareWeather`.
62
62
 
63
63
  ### GitHub Copilot / VS Code
64
64
 
@@ -34548,6 +34548,7 @@ async function httpJson(url2, options = {}) {
34548
34548
  function defaultConfig(overrides = {}) {
34549
34549
  return {
34550
34550
  forecastBaseUrl: process.env.OPEN_METEO_FORECAST_URL ?? "https://api.open-meteo.com",
34551
+ archiveBaseUrl: process.env.OPEN_METEO_ARCHIVE_URL ?? "https://archive-api.open-meteo.com",
34551
34552
  geocodingBaseUrl: process.env.OPEN_METEO_GEOCODING_URL ?? "https://geocoding-api.open-meteo.com",
34552
34553
  timeoutMs: 1e4,
34553
34554
  retries: 1,
@@ -34728,8 +34729,198 @@ async function getForecast(latitude, longitude, startDate, endDate, config2 = {}
34728
34729
  };
34729
34730
  }
34730
34731
 
34732
+ // src/weather/history.ts
34733
+ var MAX_HISTORICAL_YEARS = 20;
34734
+ var ARCHIVE_FIRST_YEAR = 1940;
34735
+ var DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
34736
+ function assertHistoricalSpan(month, day, startYear, endYear) {
34737
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
34738
+ throw new ToolError("INVALID_DATE_RANGE", `month must be 1-12. Got ${month}`, false);
34739
+ }
34740
+ const maxDay = DAYS_IN_MONTH[month - 1];
34741
+ if (!Number.isInteger(day) || day < 1 || day > maxDay) {
34742
+ throw new ToolError(
34743
+ "INVALID_DATE_RANGE",
34744
+ `day must be 1-${maxDay} for month ${month}. Got ${day}`,
34745
+ false
34746
+ );
34747
+ }
34748
+ if (!Number.isInteger(startYear) || !Number.isInteger(endYear)) {
34749
+ throw new ToolError(
34750
+ "INVALID_DATE_RANGE",
34751
+ `startYear and endYear must be integers. Got ${startYear}..${endYear}`,
34752
+ false
34753
+ );
34754
+ }
34755
+ if (startYear < ARCHIVE_FIRST_YEAR) {
34756
+ throw new ToolError(
34757
+ "INVALID_DATE_RANGE",
34758
+ `startYear must be >= ${ARCHIVE_FIRST_YEAR}. Got ${startYear}`,
34759
+ false
34760
+ );
34761
+ }
34762
+ const thisYear = (/* @__PURE__ */ new Date()).getUTCFullYear();
34763
+ if (endYear > thisYear) {
34764
+ throw new ToolError(
34765
+ "INVALID_DATE_RANGE",
34766
+ `endYear must be <= ${thisYear}. Got ${endYear}`,
34767
+ false
34768
+ );
34769
+ }
34770
+ if (startYear > endYear) {
34771
+ throw new ToolError(
34772
+ "INVALID_DATE_RANGE",
34773
+ `startYear (${startYear}) must be <= endYear (${endYear})`,
34774
+ false
34775
+ );
34776
+ }
34777
+ const span = endYear - startYear + 1;
34778
+ if (span > MAX_HISTORICAL_YEARS) {
34779
+ throw new ToolError(
34780
+ "INVALID_DATE_RANGE",
34781
+ `At most ${MAX_HISTORICAL_YEARS} years per call. Got ${span} (${startYear}..${endYear})`,
34782
+ false
34783
+ );
34784
+ }
34785
+ }
34786
+ function isoDate(year, month, day) {
34787
+ const m = String(month).padStart(2, "0");
34788
+ const d = String(day).padStart(2, "0");
34789
+ return `${year}-${m}-${d}`;
34790
+ }
34791
+ function isRealCalendarDate(year, month, day) {
34792
+ const dt = new Date(Date.UTC(year, month - 1, day));
34793
+ return dt.getUTCFullYear() === year && dt.getUTCMonth() === month - 1 && dt.getUTCDate() === day;
34794
+ }
34795
+ function num(value, label) {
34796
+ if (typeof value !== "number" || Number.isNaN(value)) {
34797
+ throw new ToolError("MALFORMED_RESPONSE", `Archive field ${label} is missing`, false);
34798
+ }
34799
+ return value;
34800
+ }
34801
+ function round1(n) {
34802
+ return Math.round(n * 10) / 10;
34803
+ }
34804
+ async function mapPool(items, limit, fn) {
34805
+ const out = new Array(items.length);
34806
+ let next = 0;
34807
+ async function worker() {
34808
+ for (; ; ) {
34809
+ const i = next;
34810
+ next += 1;
34811
+ if (i >= items.length) return;
34812
+ out[i] = await fn(items[i]);
34813
+ }
34814
+ }
34815
+ const workers = Math.min(limit, items.length);
34816
+ await Promise.all(Array.from({ length: workers }, () => worker()));
34817
+ return out;
34818
+ }
34819
+ async function fetchYear(year, month, day, latitude, longitude, cfg) {
34820
+ const date5 = isoDate(year, month, day);
34821
+ if (!isRealCalendarDate(year, month, day)) {
34822
+ return { ok: false, unavailable: { year, reason: `${date5} is not a calendar date` } };
34823
+ }
34824
+ try {
34825
+ const data = await meteoGet(
34826
+ cfg.archiveBaseUrl,
34827
+ "/v1/archive",
34828
+ {
34829
+ latitude,
34830
+ longitude,
34831
+ daily: "temperature_2m_max,temperature_2m_min,temperature_2m_mean,precipitation_sum,snowfall_sum,weather_code,wind_speed_10m_max",
34832
+ timezone: "auto",
34833
+ start_date: date5,
34834
+ end_date: date5
34835
+ },
34836
+ cfg
34837
+ );
34838
+ const daily = data.daily;
34839
+ const time3 = daily?.time?.[0];
34840
+ if (!daily || !time3) {
34841
+ return { ok: false, unavailable: { year, reason: `No archive data for ${date5}` } };
34842
+ }
34843
+ const code = num(daily.weather_code?.[0], "weather_code");
34844
+ return {
34845
+ ok: true,
34846
+ timezone: data.timezone,
34847
+ latitude: data.latitude,
34848
+ longitude: data.longitude,
34849
+ row: {
34850
+ year,
34851
+ date: time3,
34852
+ tempMaxC: num(daily.temperature_2m_max?.[0], "temperature_2m_max"),
34853
+ tempMinC: num(daily.temperature_2m_min?.[0], "temperature_2m_min"),
34854
+ tempMeanC: num(daily.temperature_2m_mean?.[0], "temperature_2m_mean"),
34855
+ precipitationMm: num(daily.precipitation_sum?.[0], "precipitation_sum"),
34856
+ snowfallCm: num(daily.snowfall_sum?.[0], "snowfall_sum"),
34857
+ windSpeedMaxKmh: num(daily.wind_speed_10m_max?.[0], "wind_speed_10m_max"),
34858
+ weatherCode: code,
34859
+ weatherDescription: describeWmo(code)
34860
+ }
34861
+ };
34862
+ } catch (err) {
34863
+ const reason = err instanceof Error ? err.message : String(err);
34864
+ return { ok: false, unavailable: { year, reason } };
34865
+ }
34866
+ }
34867
+ function summarize(years) {
34868
+ const n = years.length;
34869
+ const avg = (pick2) => round1(years.reduce((sum, y) => sum + pick2(y), 0) / n);
34870
+ const wettest = years.reduce((best, y) => y.precipitationMm > best.precipitationMm ? y : best);
34871
+ const warmest = years.reduce((best, y) => y.tempMaxC > best.tempMaxC ? y : best);
34872
+ const coldest = years.reduce((best, y) => y.tempMinC < best.tempMinC ? y : best);
34873
+ return {
34874
+ avgTempMeanC: avg((y) => y.tempMeanC),
34875
+ avgTempMaxC: avg((y) => y.tempMaxC),
34876
+ avgTempMinC: avg((y) => y.tempMinC),
34877
+ rainyDays: years.filter((y) => y.precipitationMm > 0).length,
34878
+ wettestYear: wettest.year,
34879
+ wettestPrecipitationMm: wettest.precipitationMm,
34880
+ warmestYear: warmest.year,
34881
+ warmestTempMaxC: warmest.tempMaxC,
34882
+ coldestYear: coldest.year,
34883
+ coldestTempMinC: coldest.tempMinC
34884
+ };
34885
+ }
34886
+ async function getHistoricalWeather(latitude, longitude, month, day, startYear, endYear, config2 = {}) {
34887
+ assertHistoricalSpan(month, day, startYear, endYear);
34888
+ const cfg = defaultConfig(config2);
34889
+ const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i);
34890
+ const outcomes = await mapPool(
34891
+ years,
34892
+ 5,
34893
+ (year) => fetchYear(year, month, day, latitude, longitude, cfg)
34894
+ );
34895
+ const rows = outcomes.flatMap((o) => o.ok ? [o.row] : []);
34896
+ const unavailable = outcomes.flatMap((o) => o.ok ? [] : [o.unavailable]);
34897
+ if (rows.length === 0) {
34898
+ const first = unavailable[0]?.reason ?? "no data";
34899
+ throw new ToolError(
34900
+ "INVALID_DATE_RANGE",
34901
+ `Archive returned no days for ${month}/${day} in ${startYear}..${endYear}: ${first}`,
34902
+ false,
34903
+ { unavailable }
34904
+ );
34905
+ }
34906
+ const sample = outcomes.find((o) => o.ok);
34907
+ return {
34908
+ latitude: sample && sample.ok ? sample.latitude : latitude,
34909
+ longitude: sample && sample.ok ? sample.longitude : longitude,
34910
+ timezone: sample && sample.ok ? sample.timezone : "UTC",
34911
+ month,
34912
+ day,
34913
+ startYear,
34914
+ endYear,
34915
+ source: "open-meteo-archive",
34916
+ years: rows,
34917
+ unavailable,
34918
+ summary: summarize(rows)
34919
+ };
34920
+ }
34921
+
34731
34922
  // src/weather/compare.ts
34732
- function summarize(forecast) {
34923
+ function summarize2(forecast) {
34733
34924
  const n = forecast.days.length || 1;
34734
34925
  const avgTempMaxC = forecast.days.reduce((s, d) => s + d.tempMaxC, 0) / n;
34735
34926
  const avgTempMinC = forecast.days.reduce((s, d) => s + d.tempMinC, 0) / n;
@@ -34758,7 +34949,7 @@ async function compareWeather(locations, startDate, endDate, config2 = {}) {
34758
34949
  return {
34759
34950
  name: loc.name,
34760
34951
  forecast,
34761
- summary: summarize(forecast)
34952
+ summary: summarize2(forecast)
34762
34953
  };
34763
34954
  })
34764
34955
  );
@@ -34844,6 +35035,55 @@ var ForecastOutputSchema = external_exports.object({
34844
35035
  endDate: external_exports.string(),
34845
35036
  days: external_exports.array(DailyForecastSchema)
34846
35037
  });
35038
+ var HistoricalWeatherInputSchema = external_exports.object({
35039
+ latitude: external_exports.number().min(-90).max(90),
35040
+ longitude: external_exports.number().min(-180).max(180),
35041
+ month: external_exports.number().int().min(1).max(12).describe("Calendar month, 1-12"),
35042
+ day: external_exports.number().int().min(1).max(31).describe("Day of month"),
35043
+ startYear: external_exports.number().int().describe("First year, inclusive"),
35044
+ endYear: external_exports.number().int().describe("Last year, inclusive")
35045
+ });
35046
+ var HistoricalYearSchema = external_exports.object({
35047
+ year: external_exports.number(),
35048
+ date: external_exports.string(),
35049
+ tempMaxC: external_exports.number(),
35050
+ tempMinC: external_exports.number(),
35051
+ tempMeanC: external_exports.number(),
35052
+ precipitationMm: external_exports.number(),
35053
+ snowfallCm: external_exports.number(),
35054
+ windSpeedMaxKmh: external_exports.number(),
35055
+ weatherCode: external_exports.number(),
35056
+ weatherDescription: external_exports.string()
35057
+ });
35058
+ var HistoricalWeatherOutputSchema = external_exports.object({
35059
+ latitude: external_exports.number(),
35060
+ longitude: external_exports.number(),
35061
+ timezone: external_exports.string(),
35062
+ month: external_exports.number(),
35063
+ day: external_exports.number(),
35064
+ startYear: external_exports.number(),
35065
+ endYear: external_exports.number(),
35066
+ source: external_exports.literal("open-meteo-archive"),
35067
+ years: external_exports.array(HistoricalYearSchema),
35068
+ unavailable: external_exports.array(
35069
+ external_exports.object({
35070
+ year: external_exports.number(),
35071
+ reason: external_exports.string()
35072
+ })
35073
+ ),
35074
+ summary: external_exports.object({
35075
+ avgTempMeanC: external_exports.number(),
35076
+ avgTempMaxC: external_exports.number(),
35077
+ avgTempMinC: external_exports.number(),
35078
+ rainyDays: external_exports.number(),
35079
+ wettestYear: external_exports.number(),
35080
+ wettestPrecipitationMm: external_exports.number(),
35081
+ warmestYear: external_exports.number(),
35082
+ warmestTempMaxC: external_exports.number(),
35083
+ coldestYear: external_exports.number(),
35084
+ coldestTempMinC: external_exports.number()
35085
+ })
35086
+ });
34847
35087
  var CompareWeatherInputSchema = external_exports.object({
34848
35088
  locations: external_exports.array(LocationSchema).min(2).describe("Locations to compare"),
34849
35089
  startDate: external_exports.string().describe("Start date YYYY-MM-DD"),
@@ -34923,6 +35163,30 @@ function createWeatherMcpServer() {
34923
35163
  }
34924
35164
  }
34925
35165
  );
35166
+ server.registerTool(
35167
+ "getHistoricalWeather",
35168
+ {
35169
+ title: "Historical weather on a calendar day",
35170
+ description: "Get historical daily weather for the same calendar day across startYear..endYear (inclusive, at most 20 years) from the Open-Meteo archive (ERA5 reanalysis, not a single weather station). Use for questions like 'what was the weather on 28 September for the last 10 years'. Returns per-year min/mean/max temperature (\xB0C), precipitation (mm), snowfall (cm), max wind (km/h), WMO description, plus summary stats. Years that are not a real date (e.g. 29 Feb) or not yet in the archive (about a 5-day delay) are listed in unavailable. Do not use getForecast for dates older than the forecast window.",
35171
+ inputSchema: HistoricalWeatherInputSchema,
35172
+ outputSchema: HistoricalWeatherOutputSchema
35173
+ },
35174
+ async ({ latitude, longitude, month, day, startYear, endYear }) => {
35175
+ try {
35176
+ const result = await getHistoricalWeather(
35177
+ latitude,
35178
+ longitude,
35179
+ month,
35180
+ day,
35181
+ startYear,
35182
+ endYear
35183
+ );
35184
+ return okResult(result);
35185
+ } catch (err) {
35186
+ return errResult(err);
35187
+ }
35188
+ }
35189
+ );
34926
35190
  server.registerTool(
34927
35191
  "compareWeather",
34928
35192
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weather-agent-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Model-agnostic AI agent with MCP tools over Open-Meteo",
5
5
  "type": "module",
6
6
  "engines": {