docs: correctness and coverage pass for the 2026-07-28 revision (#2389)
This commit is contained in:
parent
36055d5bbd
commit
199a28e9e2
1 changed files with 1335 additions and 390 deletions
|
|
@ -1,16 +1,10 @@
|
|||
//#region prelude
|
||||
import { McpServer } from '@modelcontextprotocol/server';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
||||
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const NWS_API_BASE = 'https://api.weather.gov';
|
||||
const USER_AGENT = 'weather-app/1.0';
|
||||
|
||||
// Create server instance
|
||||
const server = new McpServer({
|
||||
name: 'weather',
|
||||
version: '1.0.0',
|
||||
});
|
||||
//#endregion prelude
|
||||
|
||||
//#region helpers
|
||||
|
|
@ -83,140 +77,147 @@ interface ForecastResponse {
|
|||
//#endregion helpers
|
||||
|
||||
//#region registerTools
|
||||
// Register weather tools
|
||||
server.registerTool(
|
||||
'get-alerts',
|
||||
{
|
||||
title: 'Get Weather Alerts',
|
||||
description: 'Get weather alerts for a state',
|
||||
inputSchema: z.object({
|
||||
state: z.string().length(2)
|
||||
.describe('Two-letter state code (e.g. CA, NY)'),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const stateCode = state.toUpperCase();
|
||||
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
|
||||
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
|
||||
// Create a server with both weather tools registered. The serving entry calls
|
||||
// this factory to build the instance it serves, so keep it cheap and
|
||||
// side-effect-free.
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
name: 'weather',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
server.registerTool(
|
||||
'get-alerts',
|
||||
{
|
||||
title: 'Get Weather Alerts',
|
||||
description: 'Get weather alerts for a state',
|
||||
inputSchema: z.object({
|
||||
state: z.string().length(2)
|
||||
.describe('Two-letter state code (e.g. CA, NY)'),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const stateCode = state.toUpperCase();
|
||||
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
|
||||
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
|
||||
|
||||
if (!alertsData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Failed to retrieve alerts data',
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const features = alertsData.features || [];
|
||||
|
||||
if (features.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `No active alerts for ${stateCode}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const formattedAlerts = features.map(formatAlert);
|
||||
|
||||
if (!alertsData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'Failed to retrieve alerts data',
|
||||
type: 'text',
|
||||
text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const features = alertsData.features || [];
|
||||
server.registerTool(
|
||||
'get-forecast',
|
||||
{
|
||||
title: 'Get Weather Forecast',
|
||||
description: 'Get weather forecast for a location',
|
||||
inputSchema: z.object({
|
||||
latitude: z.number().min(-90).max(90)
|
||||
.describe('Latitude of the location'),
|
||||
longitude: z.number().min(-180).max(180)
|
||||
.describe('Longitude of the location'),
|
||||
}),
|
||||
},
|
||||
async ({ latitude, longitude }) => {
|
||||
// Get grid point data
|
||||
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
||||
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
|
||||
|
||||
if (!pointsData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const forecastUrl = pointsData.properties?.forecast;
|
||||
if (!forecastUrl) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Failed to get forecast URL from grid point data',
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Get forecast data
|
||||
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
|
||||
if (!forecastData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'Failed to retrieve forecast data',
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const periods = forecastData.properties?.periods || [];
|
||||
if (periods.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'No forecast periods available',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
// Format forecast periods
|
||||
const formattedForecast = periods.map((period: ForecastPeriod) =>
|
||||
[
|
||||
`${period.name || 'Unknown'}:`,
|
||||
`Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`,
|
||||
`Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`,
|
||||
`${period.shortForecast || 'No forecast available'}`,
|
||||
'---',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
if (features.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `No active alerts for ${stateCode}`,
|
||||
type: 'text',
|
||||
text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const formattedAlerts = features.map(formatAlert);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'get-forecast',
|
||||
{
|
||||
title: 'Get Weather Forecast',
|
||||
description: 'Get weather forecast for a location',
|
||||
inputSchema: z.object({
|
||||
latitude: z.number().min(-90).max(90)
|
||||
.describe('Latitude of the location'),
|
||||
longitude: z.number().min(-180).max(180)
|
||||
.describe('Longitude of the location'),
|
||||
}),
|
||||
},
|
||||
async ({ latitude, longitude }) => {
|
||||
// Get grid point data
|
||||
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
|
||||
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
|
||||
|
||||
if (!pointsData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const forecastUrl = pointsData.properties?.forecast;
|
||||
if (!forecastUrl) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'Failed to get forecast URL from grid point data',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
// Get forecast data
|
||||
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
|
||||
if (!forecastData) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'Failed to retrieve forecast data',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const periods = forecastData.properties?.periods || [];
|
||||
if (periods.length === 0) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: 'No forecast periods available',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
// Format forecast periods
|
||||
const formattedForecast = periods.map((period: ForecastPeriod) =>
|
||||
[
|
||||
`${period.name || 'Unknown'}:`,
|
||||
`Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`,
|
||||
`Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`,
|
||||
`${period.shortForecast || 'No forecast available'}`,
|
||||
'---',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
);
|
||||
return server;
|
||||
}
|
||||
//#endregion registerTools
|
||||
|
||||
//#region main
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('Weather MCP Server running on stdio');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
void serveStdio(createServer);
|
||||
console.error('Weather MCP Server running on stdio');
|
||||
//#endregion main
|
||||
|
|
|
|||
Loading…
Reference in a new issue