mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
9c46f31c57
Two thirds of be-weather-airports-fold. weatherService (513 LOC, a process cache and a setInterval at import) is left for its own PR. airportService split along the line that was already there. The dataset lookup touches no database — it reads one JSON file and keeps it in memory — and both MCP registrars call it from outside the container, so findByIata/searchAirports are free functions in nest/airports/airports.data.ts. backfillFlightEndpoints needs the connection and became a method on AirportsService. That backfill was the interesting part. db/database.ts ran it at module eval through a bare require() back into services/, so the module graph closed a cycle that only held because `db` happened to be initialised by the time the require resolved. It runs in onApplicationBootstrap now: it repairs rows, nothing boots on it, and the container is fully built by then. The cycle had a visible cost. tests/setup.console-noise.ts carried a suppression for `Cannot find module '../services/airportService'`, because vitest's transform pipeline could not resolve that require at all — it fired in roughly sixty test files. That suppression is now narrowed to the only case left: an e2e TestingModule whose fixture DB has no reservations table. distanceService split the same way: haversineKm is arithmetic and moved to common/geo.ts; getFlightDistanceKm reads the database and became a private method on AuthService, its only caller. AirportsModule imports DatabaseModule explicitly. @Global only reaches modules that are in the graph, and the airports e2e builds its container from AirportsModule alone — exactly the trap the nest README now documents. The search ranking gets its first direct tests. It sat in services/, outside the coverage gate, and only ever ran incidentally through the route; moving it into src/nest made the gap measurable (16% branches). The ten cases pin the scoring ladder — exact IATA short-circuit, ICAO over prefix, IATA prefix over city prefix, city prefix over substring — and the missing-dataset fallback, which no test had reached. 90% branches now.
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
import { z } from 'zod';
|
|
import { findByIata, searchAirports } from '../../nest/airports/airports.data';
|
|
import { getWeather, getDetailedWeather } from '../../services/weatherService';
|
|
import {
|
|
TOOL_ANNOTATIONS_READONLY,
|
|
ok,
|
|
} from './_shared';
|
|
import { canRead } from '../scopes';
|
|
|
|
export function registerMapsWeatherTools(server: McpServer, _userId: number, scopes: string[] | null): void {
|
|
const canGeo = canRead(scopes, 'geo');
|
|
const canWeather = canRead(scopes, 'weather');
|
|
|
|
// The geo tools (get_place_details, reverse_geocode, resolve_maps_url) moved to
|
|
// the DI-discovered src/nest/maps/maps.mcp.ts (@McpController, attached via the
|
|
// nest-mcp registry).
|
|
|
|
// --- WEATHER ---
|
|
|
|
if (canWeather) server.registerTool(
|
|
'get_weather',
|
|
{
|
|
description: 'Get weather forecast for a location and date.',
|
|
inputSchema: {
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
date: z.string().describe('ISO date YYYY-MM-DD'),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ lat, lng, date, lang }) => {
|
|
try {
|
|
const weather = await getWeather(String(lat), String(lng), date, lang ?? 'en');
|
|
return ok({ weather });
|
|
} catch (err: any) {
|
|
return { content: [{ type: 'text' as const, text: err?.message ?? 'Weather service not available.' }], isError: true };
|
|
}
|
|
}
|
|
);
|
|
|
|
if (canWeather) server.registerTool(
|
|
'get_detailed_weather',
|
|
{
|
|
description: 'Get hourly/detailed weather forecast for a location and date.',
|
|
inputSchema: {
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
date: z.string().describe('ISO date YYYY-MM-DD'),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ lat, lng, date, lang }) => {
|
|
try {
|
|
const weather = await getDetailedWeather(String(lat), String(lng), date, lang ?? 'en');
|
|
return ok({ weather });
|
|
} catch (err: any) {
|
|
return { content: [{ type: 'text' as const, text: err?.message ?? 'Weather service not available.' }], isError: true };
|
|
}
|
|
}
|
|
);
|
|
|
|
// --- AIRPORTS ---
|
|
|
|
if (canGeo) server.registerTool(
|
|
'search_airports',
|
|
{
|
|
description: 'Search for airports by name, city, or IATA code. Returns matching airports with IATA code, name, city, country, coordinates, and timezone. Use before create_transport (flight) to get the correct IATA code and timezone for endpoints.',
|
|
inputSchema: {
|
|
query: z.string().min(1).max(200).describe('Airport name, city, or IATA code (e.g. "zurich", "ZRH", "charles de gaulle")'),
|
|
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ query, limit }) => {
|
|
const airports = searchAirports(query, limit ?? 10);
|
|
return ok({ airports });
|
|
}
|
|
);
|
|
|
|
if (canGeo) server.registerTool(
|
|
'get_airport',
|
|
{
|
|
description: 'Get a single airport by its IATA code. Returns name, city, country, coordinates, and timezone.',
|
|
inputSchema: {
|
|
iata: z.string().length(3).toUpperCase().describe('IATA airport code (e.g. "ZRH", "AMS", "CDG")'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ iata }) => {
|
|
const airport = findByIata(iata);
|
|
if (!airport) return { content: [{ type: 'text' as const, text: 'Airport not found.' }], isError: true };
|
|
return ok({ airport });
|
|
}
|
|
);
|
|
}
|