weather-agent-mcp 0.1.4 → 0.1.6
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 +610 -11
- 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`. Для ряда температур (прогноз, архив одного дня по годам, сравнение) к ответу прилагается PNG-график.
|
|
62
62
|
|
|
63
63
|
### GitHub Copilot / VS Code
|
|
64
64
|
|
package/dist/weather-mcp.js
CHANGED
|
@@ -25217,12 +25217,12 @@ var ReadBuffer = class {
|
|
|
25217
25217
|
constructor(options) {
|
|
25218
25218
|
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
25219
25219
|
}
|
|
25220
|
-
append(
|
|
25221
|
-
if ((this._buffer?.length ?? 0) +
|
|
25220
|
+
append(chunk2) {
|
|
25221
|
+
if ((this._buffer?.length ?? 0) + chunk2.length > this._maxBufferSize) {
|
|
25222
25222
|
this.clear();
|
|
25223
25223
|
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
25224
25224
|
}
|
|
25225
|
-
this._buffer = this._buffer ? Buffer.concat([this._buffer,
|
|
25225
|
+
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
|
|
25226
25226
|
}
|
|
25227
25227
|
readMessage() {
|
|
25228
25228
|
while (this._buffer) {
|
|
@@ -33905,9 +33905,9 @@ var StdioServerTransport = class {
|
|
|
33905
33905
|
onclose;
|
|
33906
33906
|
onerror;
|
|
33907
33907
|
onmessage;
|
|
33908
|
-
_ondata = (
|
|
33908
|
+
_ondata = (chunk2) => {
|
|
33909
33909
|
try {
|
|
33910
|
-
this._readBuffer.append(
|
|
33910
|
+
this._readBuffer.append(chunk2);
|
|
33911
33911
|
this.processReadBuffer();
|
|
33912
33912
|
} catch (error62) {
|
|
33913
33913
|
this.onerror?.(error62);
|
|
@@ -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,13 +34949,300 @@ 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
|
);
|
|
34765
34956
|
return { startDate, endDate, locations: results };
|
|
34766
34957
|
}
|
|
34767
34958
|
|
|
34959
|
+
// src/weather/temperature-chart.ts
|
|
34960
|
+
import { deflateSync } from "node:zlib";
|
|
34961
|
+
var CHART_WIDTH = 720;
|
|
34962
|
+
var CHART_HEIGHT = 360;
|
|
34963
|
+
var BG = [247, 244, 239];
|
|
34964
|
+
var PLOT = [255, 252, 247];
|
|
34965
|
+
var GRID = [220, 214, 204];
|
|
34966
|
+
var INK = [70, 64, 56];
|
|
34967
|
+
var SERIES_COLORS = [
|
|
34968
|
+
[196, 90, 58],
|
|
34969
|
+
[47, 111, 163],
|
|
34970
|
+
[62, 122, 90],
|
|
34971
|
+
[140, 90, 160],
|
|
34972
|
+
[176, 132, 48]
|
|
34973
|
+
];
|
|
34974
|
+
var PAD_L = 56;
|
|
34975
|
+
var PAD_R = 20;
|
|
34976
|
+
var PAD_T = 40;
|
|
34977
|
+
var PAD_B = 44;
|
|
34978
|
+
function pickAxisLabels(count, maxLabels = 6) {
|
|
34979
|
+
if (count <= 0) return [];
|
|
34980
|
+
if (count <= maxLabels) return Array.from({ length: count }, (_, i) => i);
|
|
34981
|
+
const indices = /* @__PURE__ */ new Set([0, count - 1]);
|
|
34982
|
+
const inner = maxLabels - 1;
|
|
34983
|
+
for (let i = 1; i < inner; i++) {
|
|
34984
|
+
indices.add(Math.round(i * (count - 1) / inner));
|
|
34985
|
+
}
|
|
34986
|
+
return [...indices].sort((a, b) => a - b);
|
|
34987
|
+
}
|
|
34988
|
+
function renderTemperatureChart(labels, series) {
|
|
34989
|
+
if (labels.length < 2 || series.length === 0) return null;
|
|
34990
|
+
const aligned = series.map((s) => ({ name: s.name, values: s.values.slice(0, labels.length) })).filter((s) => finiteCount(s.values) >= 2);
|
|
34991
|
+
if (aligned.length === 0) return null;
|
|
34992
|
+
const finite = aligned.flatMap((s) => s.values.filter((v) => isFiniteNumber(v)));
|
|
34993
|
+
let yMin = Math.min(...finite);
|
|
34994
|
+
let yMax = Math.max(...finite);
|
|
34995
|
+
if (yMax - yMin < 1) {
|
|
34996
|
+
yMin -= 1;
|
|
34997
|
+
yMax += 1;
|
|
34998
|
+
} else {
|
|
34999
|
+
const pad = (yMax - yMin) * 0.08;
|
|
35000
|
+
yMin -= pad;
|
|
35001
|
+
yMax += pad;
|
|
35002
|
+
}
|
|
35003
|
+
const pixels = new Uint8Array(CHART_WIDTH * CHART_HEIGHT * 3);
|
|
35004
|
+
fill(pixels, BG);
|
|
35005
|
+
const plotW = CHART_WIDTH - PAD_L - PAD_R;
|
|
35006
|
+
const plotH = CHART_HEIGHT - PAD_T - PAD_B;
|
|
35007
|
+
fillRect(pixels, PAD_L, PAD_T, plotW, plotH, PLOT);
|
|
35008
|
+
const yTicks = 4;
|
|
35009
|
+
for (let t = 0; t <= yTicks; t++) {
|
|
35010
|
+
const y = PAD_T + Math.round(plotH * t / yTicks);
|
|
35011
|
+
hLine(pixels, PAD_L, PAD_L + plotW, y, GRID);
|
|
35012
|
+
const value = yMax - (yMax - yMin) * t / yTicks;
|
|
35013
|
+
drawText(pixels, formatTemp(value), 4, y - 4, INK, 1);
|
|
35014
|
+
}
|
|
35015
|
+
vLine(pixels, PAD_L, PAD_T, PAD_T + plotH, INK);
|
|
35016
|
+
hLine(pixels, PAD_L, PAD_L + plotW, PAD_T + plotH, INK);
|
|
35017
|
+
const xIndex = (i) => PAD_L + Math.round(i * plotW / Math.max(labels.length - 1, 1));
|
|
35018
|
+
const yOf = (v) => PAD_T + Math.round(plotH * (1 - (v - yMin) / (yMax - yMin)));
|
|
35019
|
+
for (const index of pickAxisLabels(labels.length)) {
|
|
35020
|
+
const x = xIndex(index);
|
|
35021
|
+
vLine(pixels, x, PAD_T + plotH, PAD_T + plotH + 4, INK);
|
|
35022
|
+
const caption = labels[index] ?? "";
|
|
35023
|
+
drawText(pixels, caption, x - textWidth(caption, 1) / 2, PAD_T + plotH + 8, INK, 1);
|
|
35024
|
+
}
|
|
35025
|
+
aligned.forEach((s, seriesIndex) => {
|
|
35026
|
+
const color = SERIES_COLORS[seriesIndex % SERIES_COLORS.length];
|
|
35027
|
+
let prev = null;
|
|
35028
|
+
for (let i = 0; i < labels.length; i++) {
|
|
35029
|
+
const value = s.values[i];
|
|
35030
|
+
if (!isFiniteNumber(value)) {
|
|
35031
|
+
prev = null;
|
|
35032
|
+
continue;
|
|
35033
|
+
}
|
|
35034
|
+
const point = { x: xIndex(i), y: yOf(value) };
|
|
35035
|
+
if (prev) drawLine(pixels, prev.x, prev.y, point.x, point.y, color);
|
|
35036
|
+
fillRect(pixels, point.x - 2, point.y - 2, 5, 5, color);
|
|
35037
|
+
prev = point;
|
|
35038
|
+
}
|
|
35039
|
+
});
|
|
35040
|
+
let legendX = PAD_L;
|
|
35041
|
+
for (let i = 0; i < aligned.length; i++) {
|
|
35042
|
+
const color = SERIES_COLORS[i % SERIES_COLORS.length];
|
|
35043
|
+
const name = aligned[i].name;
|
|
35044
|
+
fillRect(pixels, legendX, 14, 14, 8, color);
|
|
35045
|
+
drawText(pixels, name, legendX + 18, 12, INK, 2);
|
|
35046
|
+
legendX += 18 + textWidth(name, 2) + 16;
|
|
35047
|
+
}
|
|
35048
|
+
return encodePng(pixels, CHART_WIDTH, CHART_HEIGHT);
|
|
35049
|
+
}
|
|
35050
|
+
function isFiniteNumber(value) {
|
|
35051
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
35052
|
+
}
|
|
35053
|
+
function finiteCount(values) {
|
|
35054
|
+
return values.filter((v) => isFiniteNumber(v)).length;
|
|
35055
|
+
}
|
|
35056
|
+
function formatTemp(value) {
|
|
35057
|
+
const rounded = Math.round(value);
|
|
35058
|
+
return `${rounded}`;
|
|
35059
|
+
}
|
|
35060
|
+
function fill(pixels, color) {
|
|
35061
|
+
for (let i = 0; i < pixels.length; i += 3) {
|
|
35062
|
+
pixels[i] = color[0];
|
|
35063
|
+
pixels[i + 1] = color[1];
|
|
35064
|
+
pixels[i + 2] = color[2];
|
|
35065
|
+
}
|
|
35066
|
+
}
|
|
35067
|
+
function setPixel(pixels, x, y, color) {
|
|
35068
|
+
if (x < 0 || y < 0 || x >= CHART_WIDTH || y >= CHART_HEIGHT) return;
|
|
35069
|
+
const i = (y * CHART_WIDTH + x) * 3;
|
|
35070
|
+
pixels[i] = color[0];
|
|
35071
|
+
pixels[i + 1] = color[1];
|
|
35072
|
+
pixels[i + 2] = color[2];
|
|
35073
|
+
}
|
|
35074
|
+
function fillRect(pixels, x, y, w, h, color) {
|
|
35075
|
+
for (let yy = y; yy < y + h; yy++) {
|
|
35076
|
+
for (let xx = x; xx < x + w; xx++) setPixel(pixels, xx, yy, color);
|
|
35077
|
+
}
|
|
35078
|
+
}
|
|
35079
|
+
function hLine(pixels, x0, x1, y, color) {
|
|
35080
|
+
const from = Math.min(x0, x1);
|
|
35081
|
+
const to = Math.max(x0, x1);
|
|
35082
|
+
for (let x = from; x <= to; x++) setPixel(pixels, x, y, color);
|
|
35083
|
+
}
|
|
35084
|
+
function vLine(pixels, x, y0, y1, color) {
|
|
35085
|
+
const from = Math.min(y0, y1);
|
|
35086
|
+
const to = Math.max(y0, y1);
|
|
35087
|
+
for (let y = from; y <= to; y++) setPixel(pixels, x, y, color);
|
|
35088
|
+
}
|
|
35089
|
+
function drawLine(pixels, x0, y0, x1, y1, color) {
|
|
35090
|
+
let x = x0;
|
|
35091
|
+
let y = y0;
|
|
35092
|
+
const dx = Math.abs(x1 - x0);
|
|
35093
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
35094
|
+
const dy = -Math.abs(y1 - y0);
|
|
35095
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
35096
|
+
let err = dx + dy;
|
|
35097
|
+
for (; ; ) {
|
|
35098
|
+
fillRect(pixels, x - 1, y - 1, 3, 3, color);
|
|
35099
|
+
if (x === x1 && y === y1) break;
|
|
35100
|
+
const e2 = 2 * err;
|
|
35101
|
+
if (e2 >= dy) {
|
|
35102
|
+
err += dy;
|
|
35103
|
+
x += sx;
|
|
35104
|
+
}
|
|
35105
|
+
if (e2 <= dx) {
|
|
35106
|
+
err += dx;
|
|
35107
|
+
y += sy;
|
|
35108
|
+
}
|
|
35109
|
+
}
|
|
35110
|
+
}
|
|
35111
|
+
var FONT = {
|
|
35112
|
+
" ": [0, 0, 0, 0, 0, 0, 0],
|
|
35113
|
+
"-": [0, 0, 0, 31, 0, 0, 0],
|
|
35114
|
+
".": [0, 0, 0, 0, 0, 12, 12],
|
|
35115
|
+
"/": [1, 2, 4, 8, 16, 0, 0],
|
|
35116
|
+
"0": [14, 17, 19, 21, 25, 17, 14],
|
|
35117
|
+
"1": [4, 12, 4, 4, 4, 4, 14],
|
|
35118
|
+
"2": [14, 17, 1, 2, 4, 8, 31],
|
|
35119
|
+
"3": [30, 1, 1, 14, 1, 1, 30],
|
|
35120
|
+
"4": [2, 6, 10, 18, 31, 2, 2],
|
|
35121
|
+
"5": [31, 16, 16, 30, 1, 1, 30],
|
|
35122
|
+
"6": [14, 16, 16, 30, 17, 17, 14],
|
|
35123
|
+
"7": [31, 1, 2, 4, 8, 8, 8],
|
|
35124
|
+
"8": [14, 17, 17, 14, 17, 17, 14],
|
|
35125
|
+
"9": [14, 17, 17, 15, 1, 1, 14],
|
|
35126
|
+
A: [14, 17, 17, 31, 17, 17, 17],
|
|
35127
|
+
B: [30, 17, 17, 30, 17, 17, 30],
|
|
35128
|
+
C: [14, 17, 16, 16, 16, 17, 14],
|
|
35129
|
+
D: [30, 17, 17, 17, 17, 17, 30],
|
|
35130
|
+
E: [31, 16, 16, 30, 16, 16, 31],
|
|
35131
|
+
F: [31, 16, 16, 30, 16, 16, 16],
|
|
35132
|
+
G: [14, 17, 16, 23, 17, 17, 14],
|
|
35133
|
+
H: [17, 17, 17, 31, 17, 17, 17],
|
|
35134
|
+
I: [14, 4, 4, 4, 4, 4, 14],
|
|
35135
|
+
J: [7, 2, 2, 2, 2, 18, 12],
|
|
35136
|
+
K: [17, 18, 20, 24, 20, 18, 17],
|
|
35137
|
+
L: [16, 16, 16, 16, 16, 16, 31],
|
|
35138
|
+
M: [17, 27, 21, 21, 17, 17, 17],
|
|
35139
|
+
N: [17, 25, 21, 19, 17, 17, 17],
|
|
35140
|
+
O: [14, 17, 17, 17, 17, 17, 14],
|
|
35141
|
+
P: [30, 17, 17, 30, 16, 16, 16],
|
|
35142
|
+
Q: [14, 17, 17, 17, 21, 18, 13],
|
|
35143
|
+
R: [30, 17, 17, 30, 20, 18, 17],
|
|
35144
|
+
S: [15, 16, 16, 14, 1, 1, 30],
|
|
35145
|
+
T: [31, 4, 4, 4, 4, 4, 4],
|
|
35146
|
+
U: [17, 17, 17, 17, 17, 17, 14],
|
|
35147
|
+
V: [17, 17, 17, 17, 17, 10, 4],
|
|
35148
|
+
W: [17, 17, 17, 21, 21, 21, 10],
|
|
35149
|
+
X: [17, 17, 10, 4, 10, 17, 17],
|
|
35150
|
+
Y: [17, 17, 10, 4, 4, 4, 4],
|
|
35151
|
+
Z: [31, 1, 2, 4, 8, 16, 31],
|
|
35152
|
+
a: [0, 0, 14, 1, 15, 17, 15],
|
|
35153
|
+
b: [16, 16, 30, 17, 17, 17, 30],
|
|
35154
|
+
c: [0, 0, 14, 16, 16, 17, 14],
|
|
35155
|
+
d: [1, 1, 15, 17, 17, 17, 15],
|
|
35156
|
+
e: [0, 0, 14, 17, 31, 16, 14],
|
|
35157
|
+
f: [6, 8, 28, 8, 8, 8, 8],
|
|
35158
|
+
g: [0, 0, 15, 17, 15, 1, 14],
|
|
35159
|
+
h: [16, 16, 22, 25, 17, 17, 17],
|
|
35160
|
+
i: [4, 0, 12, 4, 4, 4, 14],
|
|
35161
|
+
j: [2, 0, 6, 2, 2, 18, 12],
|
|
35162
|
+
k: [16, 16, 18, 20, 24, 20, 18],
|
|
35163
|
+
l: [12, 4, 4, 4, 4, 4, 14],
|
|
35164
|
+
m: [0, 0, 26, 21, 21, 17, 17],
|
|
35165
|
+
n: [0, 0, 22, 25, 17, 17, 17],
|
|
35166
|
+
o: [0, 0, 14, 17, 17, 17, 14],
|
|
35167
|
+
p: [0, 0, 30, 17, 30, 16, 16],
|
|
35168
|
+
q: [0, 0, 15, 17, 15, 1, 1],
|
|
35169
|
+
r: [0, 0, 22, 25, 16, 16, 16],
|
|
35170
|
+
s: [0, 0, 15, 16, 14, 1, 30],
|
|
35171
|
+
t: [8, 8, 28, 8, 8, 9, 6],
|
|
35172
|
+
u: [0, 0, 17, 17, 17, 19, 13],
|
|
35173
|
+
v: [0, 0, 17, 17, 17, 10, 4],
|
|
35174
|
+
w: [0, 0, 17, 17, 21, 21, 10],
|
|
35175
|
+
x: [0, 0, 17, 10, 4, 10, 17],
|
|
35176
|
+
y: [0, 0, 17, 17, 15, 1, 14],
|
|
35177
|
+
z: [0, 0, 31, 2, 4, 8, 31]
|
|
35178
|
+
};
|
|
35179
|
+
function glyph(ch) {
|
|
35180
|
+
return FONT[ch];
|
|
35181
|
+
}
|
|
35182
|
+
function textWidth(text, scale) {
|
|
35183
|
+
let width = 0;
|
|
35184
|
+
for (const ch of text) {
|
|
35185
|
+
if (!glyph(ch)) continue;
|
|
35186
|
+
width += 6 * scale;
|
|
35187
|
+
}
|
|
35188
|
+
return width;
|
|
35189
|
+
}
|
|
35190
|
+
function drawText(pixels, text, x, y, color, scale) {
|
|
35191
|
+
let cursor = Math.round(x);
|
|
35192
|
+
for (const ch of text) {
|
|
35193
|
+
const rows = glyph(ch);
|
|
35194
|
+
if (!rows) continue;
|
|
35195
|
+
for (let row = 0; row < rows.length; row++) {
|
|
35196
|
+
const bits = rows[row] ?? 0;
|
|
35197
|
+
for (let col = 0; col < 5; col++) {
|
|
35198
|
+
if ((bits & 1 << 4 - col) === 0) continue;
|
|
35199
|
+
fillRect(pixels, cursor + col * scale, y + row * scale, scale, scale, color);
|
|
35200
|
+
}
|
|
35201
|
+
}
|
|
35202
|
+
cursor += 6 * scale;
|
|
35203
|
+
}
|
|
35204
|
+
}
|
|
35205
|
+
function encodePng(pixels, width, height) {
|
|
35206
|
+
const stride = width * 3;
|
|
35207
|
+
const raw = Buffer.alloc((stride + 1) * height);
|
|
35208
|
+
for (let y = 0; y < height; y++) {
|
|
35209
|
+
const offset = y * (stride + 1);
|
|
35210
|
+
raw[offset] = 0;
|
|
35211
|
+
Buffer.from(pixels.buffer, pixels.byteOffset + y * stride, stride).copy(raw, offset + 1);
|
|
35212
|
+
}
|
|
35213
|
+
const compressed = deflateSync(raw);
|
|
35214
|
+
const ihdr = Buffer.alloc(13);
|
|
35215
|
+
ihdr.writeUInt32BE(width, 0);
|
|
35216
|
+
ihdr.writeUInt32BE(height, 4);
|
|
35217
|
+
ihdr[8] = 8;
|
|
35218
|
+
ihdr[9] = 2;
|
|
35219
|
+
return Buffer.concat([
|
|
35220
|
+
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
35221
|
+
chunk("IHDR", ihdr),
|
|
35222
|
+
chunk("IDAT", compressed),
|
|
35223
|
+
chunk("IEND", Buffer.alloc(0))
|
|
35224
|
+
]);
|
|
35225
|
+
}
|
|
35226
|
+
function chunk(type, data) {
|
|
35227
|
+
const length = Buffer.alloc(4);
|
|
35228
|
+
length.writeUInt32BE(data.length, 0);
|
|
35229
|
+
const name = Buffer.from(type, "ascii");
|
|
35230
|
+
const crc = Buffer.alloc(4);
|
|
35231
|
+
crc.writeUInt32BE(crc32(Buffer.concat([name, data])), 0);
|
|
35232
|
+
return Buffer.concat([length, name, data, crc]);
|
|
35233
|
+
}
|
|
35234
|
+
function crc32(data) {
|
|
35235
|
+
let crc = 4294967295;
|
|
35236
|
+
for (const byte of data) {
|
|
35237
|
+
crc ^= byte;
|
|
35238
|
+
for (let i = 0; i < 8; i++) {
|
|
35239
|
+
const mask = -(crc & 1);
|
|
35240
|
+
crc = crc >>> 1 ^ 3988292384 & mask;
|
|
35241
|
+
}
|
|
35242
|
+
}
|
|
35243
|
+
return (crc ^ 4294967295) >>> 0;
|
|
35244
|
+
}
|
|
35245
|
+
|
|
34768
35246
|
// src/mcp-server/error-envelope.ts
|
|
34769
35247
|
function toErrorText(err) {
|
|
34770
35248
|
const envelope = isAppError(err) ? err.toEnvelope() : {
|
|
@@ -34774,9 +35252,13 @@ function toErrorText(err) {
|
|
|
34774
35252
|
};
|
|
34775
35253
|
return JSON.stringify(envelope);
|
|
34776
35254
|
}
|
|
34777
|
-
function okResult(output2) {
|
|
35255
|
+
function okResult(output2, image) {
|
|
35256
|
+
const content = [{ type: "text", text: JSON.stringify(output2) }];
|
|
35257
|
+
if (image) {
|
|
35258
|
+
content.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
35259
|
+
}
|
|
34778
35260
|
return {
|
|
34779
|
-
content
|
|
35261
|
+
content,
|
|
34780
35262
|
structuredContent: output2
|
|
34781
35263
|
};
|
|
34782
35264
|
}
|
|
@@ -34844,6 +35326,55 @@ var ForecastOutputSchema = external_exports.object({
|
|
|
34844
35326
|
endDate: external_exports.string(),
|
|
34845
35327
|
days: external_exports.array(DailyForecastSchema)
|
|
34846
35328
|
});
|
|
35329
|
+
var HistoricalWeatherInputSchema = external_exports.object({
|
|
35330
|
+
latitude: external_exports.number().min(-90).max(90),
|
|
35331
|
+
longitude: external_exports.number().min(-180).max(180),
|
|
35332
|
+
month: external_exports.number().int().min(1).max(12).describe("Calendar month, 1-12"),
|
|
35333
|
+
day: external_exports.number().int().min(1).max(31).describe("Day of month"),
|
|
35334
|
+
startYear: external_exports.number().int().describe("First year, inclusive"),
|
|
35335
|
+
endYear: external_exports.number().int().describe("Last year, inclusive")
|
|
35336
|
+
});
|
|
35337
|
+
var HistoricalYearSchema = external_exports.object({
|
|
35338
|
+
year: external_exports.number(),
|
|
35339
|
+
date: external_exports.string(),
|
|
35340
|
+
tempMaxC: external_exports.number(),
|
|
35341
|
+
tempMinC: external_exports.number(),
|
|
35342
|
+
tempMeanC: external_exports.number(),
|
|
35343
|
+
precipitationMm: external_exports.number(),
|
|
35344
|
+
snowfallCm: external_exports.number(),
|
|
35345
|
+
windSpeedMaxKmh: external_exports.number(),
|
|
35346
|
+
weatherCode: external_exports.number(),
|
|
35347
|
+
weatherDescription: external_exports.string()
|
|
35348
|
+
});
|
|
35349
|
+
var HistoricalWeatherOutputSchema = external_exports.object({
|
|
35350
|
+
latitude: external_exports.number(),
|
|
35351
|
+
longitude: external_exports.number(),
|
|
35352
|
+
timezone: external_exports.string(),
|
|
35353
|
+
month: external_exports.number(),
|
|
35354
|
+
day: external_exports.number(),
|
|
35355
|
+
startYear: external_exports.number(),
|
|
35356
|
+
endYear: external_exports.number(),
|
|
35357
|
+
source: external_exports.literal("open-meteo-archive"),
|
|
35358
|
+
years: external_exports.array(HistoricalYearSchema),
|
|
35359
|
+
unavailable: external_exports.array(
|
|
35360
|
+
external_exports.object({
|
|
35361
|
+
year: external_exports.number(),
|
|
35362
|
+
reason: external_exports.string()
|
|
35363
|
+
})
|
|
35364
|
+
),
|
|
35365
|
+
summary: external_exports.object({
|
|
35366
|
+
avgTempMeanC: external_exports.number(),
|
|
35367
|
+
avgTempMaxC: external_exports.number(),
|
|
35368
|
+
avgTempMinC: external_exports.number(),
|
|
35369
|
+
rainyDays: external_exports.number(),
|
|
35370
|
+
wettestYear: external_exports.number(),
|
|
35371
|
+
wettestPrecipitationMm: external_exports.number(),
|
|
35372
|
+
warmestYear: external_exports.number(),
|
|
35373
|
+
warmestTempMaxC: external_exports.number(),
|
|
35374
|
+
coldestYear: external_exports.number(),
|
|
35375
|
+
coldestTempMinC: external_exports.number()
|
|
35376
|
+
})
|
|
35377
|
+
});
|
|
34847
35378
|
var CompareWeatherInputSchema = external_exports.object({
|
|
34848
35379
|
locations: external_exports.array(LocationSchema).min(2).describe("Locations to compare"),
|
|
34849
35380
|
startDate: external_exports.string().describe("Start date YYYY-MM-DD"),
|
|
@@ -34917,7 +35448,31 @@ function createWeatherMcpServer() {
|
|
|
34917
35448
|
async ({ latitude, longitude, startDate, endDate }) => {
|
|
34918
35449
|
try {
|
|
34919
35450
|
const result = await getForecast(latitude, longitude, startDate, endDate);
|
|
34920
|
-
return okResult(result);
|
|
35451
|
+
return okResult(result, chartForForecast(result));
|
|
35452
|
+
} catch (err) {
|
|
35453
|
+
return errResult(err);
|
|
35454
|
+
}
|
|
35455
|
+
}
|
|
35456
|
+
);
|
|
35457
|
+
server.registerTool(
|
|
35458
|
+
"getHistoricalWeather",
|
|
35459
|
+
{
|
|
35460
|
+
title: "Historical weather on a calendar day",
|
|
35461
|
+
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.",
|
|
35462
|
+
inputSchema: HistoricalWeatherInputSchema,
|
|
35463
|
+
outputSchema: HistoricalWeatherOutputSchema
|
|
35464
|
+
},
|
|
35465
|
+
async ({ latitude, longitude, month, day, startYear, endYear }) => {
|
|
35466
|
+
try {
|
|
35467
|
+
const result = await getHistoricalWeather(
|
|
35468
|
+
latitude,
|
|
35469
|
+
longitude,
|
|
35470
|
+
month,
|
|
35471
|
+
day,
|
|
35472
|
+
startYear,
|
|
35473
|
+
endYear
|
|
35474
|
+
);
|
|
35475
|
+
return okResult(result, chartForHistory(result));
|
|
34921
35476
|
} catch (err) {
|
|
34922
35477
|
return errResult(err);
|
|
34923
35478
|
}
|
|
@@ -34934,7 +35489,7 @@ function createWeatherMcpServer() {
|
|
|
34934
35489
|
async ({ locations, startDate, endDate }) => {
|
|
34935
35490
|
try {
|
|
34936
35491
|
const result = await compareWeather(locations, startDate, endDate);
|
|
34937
|
-
return okResult(result);
|
|
35492
|
+
return okResult(result, chartForCompare(result));
|
|
34938
35493
|
} catch (err) {
|
|
34939
35494
|
return errResult(err);
|
|
34940
35495
|
}
|
|
@@ -34942,6 +35497,50 @@ function createWeatherMcpServer() {
|
|
|
34942
35497
|
);
|
|
34943
35498
|
return server;
|
|
34944
35499
|
}
|
|
35500
|
+
function pngBlock(png) {
|
|
35501
|
+
if (!png) return void 0;
|
|
35502
|
+
return { data: png.toString("base64"), mimeType: "image/png" };
|
|
35503
|
+
}
|
|
35504
|
+
function shortDate(iso) {
|
|
35505
|
+
return iso.slice(5);
|
|
35506
|
+
}
|
|
35507
|
+
function chartForForecast(result) {
|
|
35508
|
+
return pngBlock(
|
|
35509
|
+
renderTemperatureChart(
|
|
35510
|
+
result.days.map((day) => shortDate(day.date)),
|
|
35511
|
+
[
|
|
35512
|
+
{ name: "min", values: result.days.map((day) => day.tempMinC) },
|
|
35513
|
+
{ name: "max", values: result.days.map((day) => day.tempMaxC) }
|
|
35514
|
+
]
|
|
35515
|
+
)
|
|
35516
|
+
);
|
|
35517
|
+
}
|
|
35518
|
+
function chartForHistory(result) {
|
|
35519
|
+
return pngBlock(
|
|
35520
|
+
renderTemperatureChart(
|
|
35521
|
+
result.years.map((year) => String(year.year)),
|
|
35522
|
+
[
|
|
35523
|
+
{ name: "min", values: result.years.map((year) => year.tempMinC) },
|
|
35524
|
+
{ name: "mean", values: result.years.map((year) => year.tempMeanC) },
|
|
35525
|
+
{ name: "max", values: result.years.map((year) => year.tempMaxC) }
|
|
35526
|
+
]
|
|
35527
|
+
)
|
|
35528
|
+
);
|
|
35529
|
+
}
|
|
35530
|
+
function chartForCompare(result) {
|
|
35531
|
+
const labels = result.locations[0]?.forecast.days.map((day) => day.date) ?? [];
|
|
35532
|
+
return pngBlock(
|
|
35533
|
+
renderTemperatureChart(
|
|
35534
|
+
labels.map(shortDate),
|
|
35535
|
+
result.locations.map((location) => ({
|
|
35536
|
+
name: location.name,
|
|
35537
|
+
values: labels.map(
|
|
35538
|
+
(date5) => location.forecast.days.find((day) => day.date === date5)?.tempMaxC ?? null
|
|
35539
|
+
)
|
|
35540
|
+
}))
|
|
35541
|
+
)
|
|
35542
|
+
);
|
|
35543
|
+
}
|
|
34945
35544
|
|
|
34946
35545
|
// src/mcp-server/index.ts
|
|
34947
35546
|
void serveStdio(() => createWeatherMcpServer(), {
|