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 +1 -0
- package/README.md +1 -1
- package/dist/weather-mcp.js +266 -2
- package/package.json +1 -1
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
|
|
package/dist/weather-mcp.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
{
|