@squawk/mcp 0.2.0
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/README.md +301 -0
- package/dist/bin.d.ts +11 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +41 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/resolvers.d.ts +60 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/resolvers.js +94 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +92 -0
- package/dist/tools/airports.d.ts +16 -0
- package/dist/tools/airports.d.ts.map +1 -0
- package/dist/tools/airports.js +142 -0
- package/dist/tools/airspace.d.ts +15 -0
- package/dist/tools/airspace.d.ts.map +1 -0
- package/dist/tools/airspace.js +81 -0
- package/dist/tools/airways.d.ts +14 -0
- package/dist/tools/airways.d.ts.map +1 -0
- package/dist/tools/airways.js +115 -0
- package/dist/tools/datasets.d.ts +18 -0
- package/dist/tools/datasets.d.ts.map +1 -0
- package/dist/tools/datasets.js +78 -0
- package/dist/tools/fixes.d.ts +14 -0
- package/dist/tools/fixes.d.ts.map +1 -0
- package/dist/tools/fixes.js +108 -0
- package/dist/tools/flight-math.d.ts +23 -0
- package/dist/tools/flight-math.d.ts.map +1 -0
- package/dist/tools/flight-math.js +643 -0
- package/dist/tools/flightplan.d.ts +17 -0
- package/dist/tools/flightplan.d.ts.map +1 -0
- package/dist/tools/flightplan.js +64 -0
- package/dist/tools/geo.d.ts +15 -0
- package/dist/tools/geo.d.ts.map +1 -0
- package/dist/tools/geo.js +127 -0
- package/dist/tools/icao-registry.d.ts +19 -0
- package/dist/tools/icao-registry.d.ts.map +1 -0
- package/dist/tools/icao-registry.js +45 -0
- package/dist/tools/navaids.d.ts +14 -0
- package/dist/tools/navaids.d.ts.map +1 -0
- package/dist/tools/navaids.js +143 -0
- package/dist/tools/notams.d.ts +13 -0
- package/dist/tools/notams.d.ts.map +1 -0
- package/dist/tools/notams.js +29 -0
- package/dist/tools/procedures.d.ts +15 -0
- package/dist/tools/procedures.d.ts.map +1 -0
- package/dist/tools/procedures.js +120 -0
- package/dist/tools/tool-helpers.d.ts +66 -0
- package/dist/tools/tool-helpers.d.ts.map +1 -0
- package/dist/tools/tool-helpers.js +55 -0
- package/dist/tools/weather.d.ts +18 -0
- package/dist/tools/weather.d.ts.map +1 -0
- package/dist/tools/weather.js +215 -0
- package/package.json +77 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* MCP tool module wrapping `@squawk/flight-math` E6B-style flight computer
|
|
4
|
+
* calculations. Each tool corresponds to a single function from one of the
|
|
5
|
+
* package's namespaces (atmosphere, airspeed, wind, descent, navigation,
|
|
6
|
+
* turn, glide, solar, magnetic, planning).
|
|
7
|
+
*
|
|
8
|
+
* Trivially-simple operations (single multiplications, divisions, or
|
|
9
|
+
* trig calls) are intentionally not exposed as tools - LLMs handle those
|
|
10
|
+
* accurately on their own. The selected tools cover the cases where the
|
|
11
|
+
* underlying implementation embeds a non-trivial constant, formula, or
|
|
12
|
+
* lookup (the WMM2025 spherical harmonics, the NOAA solar algorithm,
|
|
13
|
+
* compressible-flow pitot equations, holding-pattern sector logic, and
|
|
14
|
+
* so on).
|
|
15
|
+
*/
|
|
16
|
+
import { airspeed, atmosphere, descent, glide, magnetic, navigation, planning, solar, turn, wind, } from '@squawk/flight-math';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
/** Reusable zod fragment describing a latitude input. */
|
|
19
|
+
const latFragment = z
|
|
20
|
+
.number()
|
|
21
|
+
.min(-90)
|
|
22
|
+
.max(90)
|
|
23
|
+
.describe('Latitude in decimal degrees (WGS84, positive north).');
|
|
24
|
+
/** Reusable zod fragment describing a longitude input. */
|
|
25
|
+
const lonFragment = z
|
|
26
|
+
.number()
|
|
27
|
+
.min(-180)
|
|
28
|
+
.max(180)
|
|
29
|
+
.describe('Longitude in decimal degrees (WGS84, positive east).');
|
|
30
|
+
/** Reusable zod fragment describing an ISO 8601 UTC datetime input. */
|
|
31
|
+
const isoDateFragment = z
|
|
32
|
+
.string()
|
|
33
|
+
.min(1)
|
|
34
|
+
.describe('UTC datetime as an ISO 8601 string (e.g. "2026-04-18T12:00:00Z").');
|
|
35
|
+
/**
|
|
36
|
+
* Parses an ISO 8601 string into a Date and validates the result. Throws when
|
|
37
|
+
* the string cannot be parsed so callers can surface the failure as an MCP
|
|
38
|
+
* error result.
|
|
39
|
+
*
|
|
40
|
+
* @param iso - ISO 8601 datetime string.
|
|
41
|
+
* @returns The parsed Date.
|
|
42
|
+
*/
|
|
43
|
+
function parseIsoDate(iso) {
|
|
44
|
+
const date = new Date(iso);
|
|
45
|
+
if (Number.isNaN(date.getTime())) {
|
|
46
|
+
throw new Error(`Invalid ISO 8601 datetime: "${iso}"`);
|
|
47
|
+
}
|
|
48
|
+
return date;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Builds a {@link MagneticFieldOptions} object from the optional altitude and
|
|
52
|
+
* ISO datetime arguments shared across the magnetic tools. Returns either the
|
|
53
|
+
* options or a parse-error message when the date string cannot be interpreted.
|
|
54
|
+
*
|
|
55
|
+
* @param altitudeFt - Optional altitude in feet MSL.
|
|
56
|
+
* @param dateUtc - Optional ISO 8601 UTC date string.
|
|
57
|
+
* @returns A discriminated union: `ok=true` with the options, or `ok=false`
|
|
58
|
+
* with the parser message.
|
|
59
|
+
*/
|
|
60
|
+
function buildMagneticOptions(altitudeFt, dateUtc) {
|
|
61
|
+
let date;
|
|
62
|
+
if (dateUtc !== undefined) {
|
|
63
|
+
try {
|
|
64
|
+
date = parseIsoDate(dateUtc);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const options = {
|
|
71
|
+
...(altitudeFt !== undefined ? { altitudeFt } : {}),
|
|
72
|
+
...(date !== undefined ? { date } : {}),
|
|
73
|
+
};
|
|
74
|
+
return { ok: true, options };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Registers flight-math computation tools on the given MCP server.
|
|
78
|
+
*
|
|
79
|
+
* @param server - The MCP server instance to register tools on.
|
|
80
|
+
*/
|
|
81
|
+
export function registerFlightMathTools(server) {
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Atmosphere
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
server.registerTool('compute_density_altitude', {
|
|
86
|
+
title: 'Compute density altitude from field observations',
|
|
87
|
+
description: 'Computes density altitude from field elevation, altimeter setting (inHg), and outside air temperature (Celsius). Uses the ISA model to derive pressure altitude from the altimeter setting, then applies the temperature correction.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
fieldElevationFt: z.number().describe('Field elevation in feet MSL.'),
|
|
90
|
+
altimeterSettingInHg: z
|
|
91
|
+
.number()
|
|
92
|
+
.positive()
|
|
93
|
+
.describe('Altimeter setting (QNH) in inches of mercury.'),
|
|
94
|
+
oatCelsius: z.number().describe('Outside air temperature in degrees Celsius.'),
|
|
95
|
+
},
|
|
96
|
+
}, ({ fieldElevationFt, altimeterSettingInHg, oatCelsius }) => {
|
|
97
|
+
const densityAltitudeFt = atmosphere.densityAltitude(fieldElevationFt, altimeterSettingInHg, oatCelsius);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: `${densityAltitudeFt.toFixed(0)} ft` }],
|
|
100
|
+
structuredContent: { densityAltitudeFt },
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
server.registerTool('compute_true_altitude', {
|
|
104
|
+
title: 'Compute true altitude from indicated altitude',
|
|
105
|
+
description: 'Computes true altitude from indicated altitude, altimeter setting (inHg), and outside air temperature (Celsius). When stationElevationFt is provided, the temperature correction is applied only to the altitude above the station; otherwise it scales the entire indicated altitude (the standard E6B method).',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
indicatedAltitudeFt: z
|
|
108
|
+
.number()
|
|
109
|
+
.describe('Indicated altitude in feet (altimeter set to QNH).'),
|
|
110
|
+
altimeterSettingInHg: z
|
|
111
|
+
.number()
|
|
112
|
+
.positive()
|
|
113
|
+
.describe('Altimeter setting (QNH) in inches of mercury.'),
|
|
114
|
+
oatCelsius: z.number().describe('Outside air temperature in degrees Celsius.'),
|
|
115
|
+
stationElevationFt: z
|
|
116
|
+
.number()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Optional elevation of the altimeter-setting station in feet MSL.'),
|
|
119
|
+
},
|
|
120
|
+
}, ({ indicatedAltitudeFt, altimeterSettingInHg, oatCelsius, stationElevationFt }) => {
|
|
121
|
+
const trueAltitudeFt = atmosphere.trueAltitude(indicatedAltitudeFt, altimeterSettingInHg, oatCelsius, stationElevationFt);
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: 'text', text: `${trueAltitudeFt.toFixed(0)} ft` }],
|
|
124
|
+
structuredContent: { trueAltitudeFt },
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Airspeed
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
server.registerTool('compute_calibrated_airspeed_from_true_airspeed', {
|
|
131
|
+
title: 'Compute CAS from TAS',
|
|
132
|
+
description: 'Converts true airspeed (TAS) to calibrated airspeed (CAS) using the full compressible-flow pitot-static equations (ICAO standard). Valid for subsonic flight (Mach < 1.0). When OAT is omitted, ISA standard temperature at the given pressure altitude is used.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
trueAirspeedKt: z.number().positive().describe('True airspeed in knots.'),
|
|
135
|
+
pressureAltitudeFt: z.number().describe('Pressure altitude in feet.'),
|
|
136
|
+
oatCelsius: z
|
|
137
|
+
.number()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe('Optional outside air temperature in degrees Celsius (defaults to ISA standard).'),
|
|
140
|
+
},
|
|
141
|
+
}, ({ trueAirspeedKt, pressureAltitudeFt, oatCelsius }) => {
|
|
142
|
+
const calibratedAirspeedKt = airspeed.calibratedAirspeedFromTrueAirspeed(trueAirspeedKt, pressureAltitudeFt, oatCelsius);
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: 'text', text: `${calibratedAirspeedKt.toFixed(1)} kt CAS` }],
|
|
145
|
+
structuredContent: { calibratedAirspeedKt },
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Wind
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
server.registerTool('solve_wind_triangle', {
|
|
152
|
+
title: 'Solve the wind triangle for heading and groundspeed',
|
|
153
|
+
description: 'Solves the forward wind triangle: given true airspeed, true course, wind direction (FROM, true), and wind speed, computes the true heading to fly, the wind correction angle (positive = crab right), and the resulting groundspeed.',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
trueAirspeedKt: z.number().positive().describe('True airspeed in knots.'),
|
|
156
|
+
trueCourseDeg: z
|
|
157
|
+
.number()
|
|
158
|
+
.min(0)
|
|
159
|
+
.max(360)
|
|
160
|
+
.describe('Desired ground track (true course) in degrees true (0-360).'),
|
|
161
|
+
windDirectionDeg: z
|
|
162
|
+
.number()
|
|
163
|
+
.min(0)
|
|
164
|
+
.max(360)
|
|
165
|
+
.describe('Direction the wind is blowing FROM in degrees true (0-360).'),
|
|
166
|
+
windSpeedKt: z.number().min(0).describe('Wind speed in knots.'),
|
|
167
|
+
},
|
|
168
|
+
}, ({ trueAirspeedKt, trueCourseDeg, windDirectionDeg, windSpeedKt }) => {
|
|
169
|
+
const { trueHeadingDeg, windCorrectionAngleDeg, groundSpeedKt } = wind.solveWindTriangle(trueAirspeedKt, trueCourseDeg, windDirectionDeg, windSpeedKt);
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: `Heading ${trueHeadingDeg.toFixed(1)} deg true, WCA ${windCorrectionAngleDeg.toFixed(1)} deg, GS ${groundSpeedKt.toFixed(1)} kt`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
structuredContent: { trueHeadingDeg, windCorrectionAngleDeg, groundSpeedKt },
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
server.registerTool('compute_headwind_crosswind', {
|
|
181
|
+
title: 'Resolve wind into headwind and crosswind components',
|
|
182
|
+
description: 'Resolves a wind into headwind (positive = headwind, negative = tailwind) and crosswind (positive = right, negative = left) components relative to a heading or runway orientation.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
windDirectionDeg: z
|
|
185
|
+
.number()
|
|
186
|
+
.min(0)
|
|
187
|
+
.max(360)
|
|
188
|
+
.describe('Direction the wind is blowing FROM in degrees true (0-360).'),
|
|
189
|
+
windSpeedKt: z.number().min(0).describe('Wind speed in knots.'),
|
|
190
|
+
headingDeg: z
|
|
191
|
+
.number()
|
|
192
|
+
.min(0)
|
|
193
|
+
.max(360)
|
|
194
|
+
.describe('Aircraft or runway heading in degrees (0-360).'),
|
|
195
|
+
},
|
|
196
|
+
}, ({ windDirectionDeg, windSpeedKt, headingDeg }) => {
|
|
197
|
+
const { headwindKt, crosswindKt } = wind.headwindCrosswind(windDirectionDeg, windSpeedKt, headingDeg);
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: `Headwind ${headwindKt.toFixed(1)} kt, crosswind ${crosswindKt.toFixed(1)} kt`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
structuredContent: { headwindKt, crosswindKt },
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
server.registerTool('find_wind_from_track', {
|
|
209
|
+
title: 'Derive wind from observed ground track',
|
|
210
|
+
description: 'Reverse wind triangle: given the observed groundspeed, true airspeed, true heading, and true ground track, computes the wind direction (FROM) and speed that explains the difference.',
|
|
211
|
+
inputSchema: {
|
|
212
|
+
groundSpeedKt: z.number().positive().describe('Observed groundspeed in knots.'),
|
|
213
|
+
trueAirspeedKt: z.number().positive().describe('True airspeed in knots.'),
|
|
214
|
+
trueHeadingDeg: z.number().min(0).max(360).describe('True heading in degrees (0-360).'),
|
|
215
|
+
trueTrackDeg: z.number().min(0).max(360).describe('True ground track in degrees (0-360).'),
|
|
216
|
+
},
|
|
217
|
+
}, ({ groundSpeedKt, trueAirspeedKt, trueHeadingDeg, trueTrackDeg }) => {
|
|
218
|
+
const { directionDeg, speedKt } = wind.findWind(groundSpeedKt, trueAirspeedKt, trueHeadingDeg, trueTrackDeg);
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: `Wind from ${directionDeg.toFixed(0)} deg at ${speedKt.toFixed(1)} kt`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
structuredContent: { directionDeg, speedKt },
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
server.registerTool('compute_crosswind_component', {
|
|
230
|
+
title: 'Compute absolute crosswind component for a runway',
|
|
231
|
+
description: "Returns the absolute crosswind component in knots for comparison against an aircraft's maximum demonstrated crosswind limit.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
windDirectionDeg: z
|
|
234
|
+
.number()
|
|
235
|
+
.min(0)
|
|
236
|
+
.max(360)
|
|
237
|
+
.describe('Direction the wind is blowing FROM in degrees true (0-360).'),
|
|
238
|
+
windSpeedKt: z.number().min(0).describe('Wind speed in knots.'),
|
|
239
|
+
runwayHeadingDeg: z.number().min(0).max(360).describe('Runway heading in degrees (0-360).'),
|
|
240
|
+
},
|
|
241
|
+
}, ({ windDirectionDeg, windSpeedKt, runwayHeadingDeg }) => {
|
|
242
|
+
const crosswindKt = wind.crosswindComponent(windDirectionDeg, windSpeedKt, runwayHeadingDeg);
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: `${crosswindKt.toFixed(1)} kt` }],
|
|
245
|
+
structuredContent: { crosswindKt },
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Descent / climb planning
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
server.registerTool('compute_top_of_descent_distance', {
|
|
252
|
+
title: 'Compute top-of-descent distance from glidepath angle',
|
|
253
|
+
description: 'Computes the horizontal distance from the target at which to begin a descent given a desired flight-path angle (e.g. 3 degrees for a standard ILS glidepath).',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
currentAltitudeFt: z.number().describe('Current altitude in feet MSL.'),
|
|
256
|
+
targetAltitudeFt: z.number().describe('Target altitude in feet MSL.'),
|
|
257
|
+
descentAngleDeg: z.number().positive().describe('Desired descent angle in degrees.'),
|
|
258
|
+
},
|
|
259
|
+
}, ({ currentAltitudeFt, targetAltitudeFt, descentAngleDeg }) => {
|
|
260
|
+
const distanceNm = descent.topOfDescent(currentAltitudeFt, targetAltitudeFt, descentAngleDeg);
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: 'text', text: `${distanceNm.toFixed(1)} nm` }],
|
|
263
|
+
structuredContent: { distanceNm },
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
server.registerTool('compute_top_of_descent_distance_from_rate', {
|
|
267
|
+
title: 'Compute top-of-descent distance from descent rate',
|
|
268
|
+
description: 'Computes the horizontal distance from the target at which to begin a descent given a desired descent rate (feet per minute) and groundspeed.',
|
|
269
|
+
inputSchema: {
|
|
270
|
+
currentAltitudeFt: z.number().describe('Current altitude in feet MSL.'),
|
|
271
|
+
targetAltitudeFt: z.number().describe('Target altitude in feet MSL.'),
|
|
272
|
+
descentRateFtPerMin: z
|
|
273
|
+
.number()
|
|
274
|
+
.positive()
|
|
275
|
+
.describe('Desired descent rate in feet per minute.'),
|
|
276
|
+
groundSpeedKt: z.number().positive().describe('Groundspeed in knots.'),
|
|
277
|
+
},
|
|
278
|
+
}, ({ currentAltitudeFt, targetAltitudeFt, descentRateFtPerMin, groundSpeedKt }) => {
|
|
279
|
+
const distanceNm = descent.topOfDescentFromRate(currentAltitudeFt, targetAltitudeFt, descentRateFtPerMin, groundSpeedKt);
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: 'text', text: `${distanceNm.toFixed(1)} nm` }],
|
|
282
|
+
structuredContent: { distanceNm },
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
server.registerTool('compute_required_descent_rate', {
|
|
286
|
+
title: 'Compute required descent rate',
|
|
287
|
+
description: 'Computes the descent rate (feet per minute) required to lose a given amount of altitude over a given distance at a given groundspeed.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
distanceNm: z.number().positive().describe('Distance to the target in nautical miles.'),
|
|
290
|
+
currentAltitudeFt: z.number().describe('Current altitude in feet MSL.'),
|
|
291
|
+
targetAltitudeFt: z.number().describe('Target altitude in feet MSL.'),
|
|
292
|
+
groundSpeedKt: z.number().positive().describe('Groundspeed in knots.'),
|
|
293
|
+
},
|
|
294
|
+
}, ({ distanceNm, currentAltitudeFt, targetAltitudeFt, groundSpeedKt }) => {
|
|
295
|
+
const descentRateFtPerMin = descent.requiredDescentRate(distanceNm, currentAltitudeFt, targetAltitudeFt, groundSpeedKt);
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: 'text', text: `${descentRateFtPerMin.toFixed(0)} ft/min` }],
|
|
298
|
+
structuredContent: { descentRateFtPerMin },
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
server.registerTool('compute_required_climb_rate', {
|
|
302
|
+
title: 'Compute required climb rate',
|
|
303
|
+
description: 'Computes the climb rate (feet per minute) required to gain a given amount of altitude over a given distance at a given groundspeed.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
distanceNm: z
|
|
306
|
+
.number()
|
|
307
|
+
.positive()
|
|
308
|
+
.describe('Distance available for climb in nautical miles.'),
|
|
309
|
+
currentAltitudeFt: z.number().describe('Current altitude in feet MSL.'),
|
|
310
|
+
targetAltitudeFt: z.number().describe('Target altitude in feet MSL.'),
|
|
311
|
+
groundSpeedKt: z.number().positive().describe('Groundspeed in knots.'),
|
|
312
|
+
},
|
|
313
|
+
}, ({ distanceNm, currentAltitudeFt, targetAltitudeFt, groundSpeedKt }) => {
|
|
314
|
+
const climbRateFtPerMin = descent.requiredClimbRate(distanceNm, currentAltitudeFt, targetAltitudeFt, groundSpeedKt);
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: 'text', text: `${climbRateFtPerMin.toFixed(0)} ft/min` }],
|
|
317
|
+
structuredContent: { climbRateFtPerMin },
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
server.registerTool('compute_visual_descent_point', {
|
|
321
|
+
title: 'Compute Visual Descent Point distance',
|
|
322
|
+
description: 'Computes the Visual Descent Point (VDP) distance from the runway threshold for a non-precision approach, given a desired glidepath angle (typically 3.0 degrees) and threshold-crossing height in feet.',
|
|
323
|
+
inputSchema: {
|
|
324
|
+
glidepathAngleDeg: z
|
|
325
|
+
.number()
|
|
326
|
+
.positive()
|
|
327
|
+
.describe('Desired glidepath angle in degrees (typically 3.0).'),
|
|
328
|
+
thresholdCrossingHeightFt: z
|
|
329
|
+
.number()
|
|
330
|
+
.positive()
|
|
331
|
+
.describe('Height above the threshold at the VDP start (feet AGL).'),
|
|
332
|
+
},
|
|
333
|
+
}, ({ glidepathAngleDeg, thresholdCrossingHeightFt }) => {
|
|
334
|
+
const distanceNm = descent.visualDescentPoint(glidepathAngleDeg, thresholdCrossingHeightFt);
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: 'text', text: `${distanceNm.toFixed(2)} nm` }],
|
|
337
|
+
structuredContent: { distanceNm },
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Navigation
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
server.registerTool('recommend_holding_pattern_entry', {
|
|
344
|
+
title: 'Recommend a holding pattern entry',
|
|
345
|
+
description: "Recommends the holding pattern entry type (direct, teardrop, or parallel) per the AIM 5-3-8 sectors, given the inbound course to the holding fix and the aircraft's heading or bearing to the fix. Defaults to right-turn holds; pass rightTurns=false for a left-turn hold.",
|
|
346
|
+
inputSchema: {
|
|
347
|
+
inboundCourseDeg: z
|
|
348
|
+
.number()
|
|
349
|
+
.min(0)
|
|
350
|
+
.max(360)
|
|
351
|
+
.describe('Inbound course to the holding fix in degrees (0-360).'),
|
|
352
|
+
headingToFixDeg: z
|
|
353
|
+
.number()
|
|
354
|
+
.min(0)
|
|
355
|
+
.max(360)
|
|
356
|
+
.describe('Aircraft heading or bearing to the fix in degrees (0-360).'),
|
|
357
|
+
rightTurns: z
|
|
358
|
+
.boolean()
|
|
359
|
+
.optional()
|
|
360
|
+
.describe('True for a right-turn hold (default), false for left-turn.'),
|
|
361
|
+
},
|
|
362
|
+
}, ({ inboundCourseDeg, headingToFixDeg, rightTurns }) => {
|
|
363
|
+
const entryType = navigation.holdingPatternEntry(inboundCourseDeg, headingToFixDeg, rightTurns);
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: 'text', text: entryType }],
|
|
366
|
+
structuredContent: { entryType },
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Turn dynamics
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
server.registerTool('compute_standard_rate_bank_angle', {
|
|
373
|
+
title: 'Compute standard-rate bank angle',
|
|
374
|
+
description: 'Computes the bank angle (degrees) required for a standard rate turn (3 degrees per second) at a given true airspeed.',
|
|
375
|
+
inputSchema: {
|
|
376
|
+
trueAirspeedKt: z.number().positive().describe('True airspeed in knots.'),
|
|
377
|
+
},
|
|
378
|
+
}, ({ trueAirspeedKt }) => {
|
|
379
|
+
const bankAngleDeg = turn.standardRateBankAngle(trueAirspeedKt);
|
|
380
|
+
return {
|
|
381
|
+
content: [{ type: 'text', text: `${bankAngleDeg.toFixed(1)} deg` }],
|
|
382
|
+
structuredContent: { bankAngleDeg },
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
server.registerTool('compute_turn_radius', {
|
|
386
|
+
title: 'Compute turn radius',
|
|
387
|
+
description: 'Computes the turn radius in nautical miles for a coordinated turn at a given true airspeed and bank angle.',
|
|
388
|
+
inputSchema: {
|
|
389
|
+
trueAirspeedKt: z.number().positive().describe('True airspeed in knots.'),
|
|
390
|
+
bankAngleDeg: z.number().positive().max(89).describe('Bank angle in degrees.'),
|
|
391
|
+
},
|
|
392
|
+
}, ({ trueAirspeedKt, bankAngleDeg }) => {
|
|
393
|
+
const turnRadiusNm = turn.turnRadius(trueAirspeedKt, bankAngleDeg);
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: 'text', text: `${turnRadiusNm.toFixed(2)} nm` }],
|
|
396
|
+
structuredContent: { turnRadiusNm },
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Glide
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
server.registerTool('compute_glide_distance_with_wind', {
|
|
403
|
+
title: 'Compute glide distance with wind correction',
|
|
404
|
+
description: 'Computes the maximum glide distance (nautical miles) from a given altitude AGL and glide ratio, scaled by the ratio of groundspeed to true airspeed during the glide. A positive headwind reduces the result; a negative value (tailwind) extends it.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
altitudeAglFt: z.number().positive().describe('Altitude above ground level in feet.'),
|
|
407
|
+
glideRatio: z.number().positive().describe('Aircraft glide ratio (e.g. 10 for 10:1).'),
|
|
408
|
+
bestGlideTasKt: z
|
|
409
|
+
.number()
|
|
410
|
+
.positive()
|
|
411
|
+
.describe('True airspeed at best glide speed in knots.'),
|
|
412
|
+
headwindKt: z
|
|
413
|
+
.number()
|
|
414
|
+
.describe('Headwind component in knots (positive = headwind, negative = tailwind).'),
|
|
415
|
+
},
|
|
416
|
+
}, ({ altitudeAglFt, glideRatio, bestGlideTasKt, headwindKt }) => {
|
|
417
|
+
const glideDistanceNm = glide.glideDistanceWithWind(altitudeAglFt, glideRatio, bestGlideTasKt, headwindKt);
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: 'text', text: `${glideDistanceNm.toFixed(1)} nm` }],
|
|
420
|
+
structuredContent: { glideDistanceNm },
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Solar (sunrise/sunset and day/night)
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
server.registerTool('compute_solar_times', {
|
|
427
|
+
title: 'Compute sunrise, sunset, and civil twilight times',
|
|
428
|
+
description: 'Computes sunrise, sunset, and civil twilight times in UTC for a given geographic position and date. Uses the NOAA solar algorithm; accurate to within roughly one minute for dates between 1901 and 2099. In polar regions, missing events are omitted from the result.',
|
|
429
|
+
inputSchema: {
|
|
430
|
+
lat: latFragment,
|
|
431
|
+
lon: lonFragment,
|
|
432
|
+
dateUtc: isoDateFragment,
|
|
433
|
+
},
|
|
434
|
+
}, ({ lat, lon, dateUtc }) => {
|
|
435
|
+
let date;
|
|
436
|
+
try {
|
|
437
|
+
date = parseIsoDate(dateUtc);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: 'text', text: message }],
|
|
443
|
+
structuredContent: { times: null },
|
|
444
|
+
isError: true,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const times = solar.computeSolarTimes(lat, lon, date);
|
|
448
|
+
const payload = {
|
|
449
|
+
sunrise: times.sunrise?.toISOString() ?? null,
|
|
450
|
+
sunset: times.sunset?.toISOString() ?? null,
|
|
451
|
+
civilTwilightBegin: times.civilTwilightBegin?.toISOString() ?? null,
|
|
452
|
+
civilTwilightEnd: times.civilTwilightEnd?.toISOString() ?? null,
|
|
453
|
+
};
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
456
|
+
structuredContent: { times: payload },
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
server.registerTool('is_daytime', {
|
|
460
|
+
title: 'Determine whether a UTC instant is daytime per FAR 1.1',
|
|
461
|
+
description: 'Returns true if the given UTC instant at the given position falls within civil twilight (the FAR 1.1 definition of daytime: between the beginning of morning civil twilight and the end of evening civil twilight). In polar regions where civil twilight does not occur, returns true if the sun is continuously above the civil twilight angle for the day, false otherwise.',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
lat: latFragment,
|
|
464
|
+
lon: lonFragment,
|
|
465
|
+
dateTimeUtc: isoDateFragment,
|
|
466
|
+
},
|
|
467
|
+
}, ({ lat, lon, dateTimeUtc }) => {
|
|
468
|
+
let date;
|
|
469
|
+
try {
|
|
470
|
+
date = parseIsoDate(dateTimeUtc);
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: 'text', text: message }],
|
|
476
|
+
structuredContent: { isDaytime: null },
|
|
477
|
+
isError: true,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const result = solar.isDaytime(lat, lon, date);
|
|
481
|
+
return {
|
|
482
|
+
content: [{ type: 'text', text: result ? 'daytime' : 'nighttime' }],
|
|
483
|
+
structuredContent: { isDaytime: result },
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Magnetic (WMM2025)
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
server.registerTool('compute_magnetic_declination', {
|
|
490
|
+
title: 'Compute magnetic declination (WMM2025)',
|
|
491
|
+
description: 'Computes magnetic declination (the angle between true and magnetic north) at a geographic position using the World Magnetic Model 2025. Positive values mean magnetic north is east of true north; negative means west. The model is valid from 2025.0 through 2030.0.',
|
|
492
|
+
inputSchema: {
|
|
493
|
+
lat: latFragment,
|
|
494
|
+
lon: lonFragment,
|
|
495
|
+
altitudeFt: z.number().optional().describe('Optional altitude in feet MSL (default 0).'),
|
|
496
|
+
dateUtc: z
|
|
497
|
+
.string()
|
|
498
|
+
.optional()
|
|
499
|
+
.describe('Optional ISO 8601 UTC date for the lookup. Defaults to the current date.'),
|
|
500
|
+
},
|
|
501
|
+
}, ({ lat, lon, altitudeFt, dateUtc }) => {
|
|
502
|
+
const built = buildMagneticOptions(altitudeFt, dateUtc);
|
|
503
|
+
if (!built.ok) {
|
|
504
|
+
return {
|
|
505
|
+
content: [{ type: 'text', text: built.message }],
|
|
506
|
+
structuredContent: { declinationDeg: null },
|
|
507
|
+
isError: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const declinationDeg = magnetic.magneticDeclination(lat, lon, built.options);
|
|
511
|
+
return {
|
|
512
|
+
content: [{ type: 'text', text: `${declinationDeg.toFixed(2)} deg` }],
|
|
513
|
+
structuredContent: { declinationDeg },
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
server.registerTool('convert_true_to_magnetic_bearing', {
|
|
517
|
+
title: 'Convert true bearing to magnetic',
|
|
518
|
+
description: 'Converts a true bearing or heading to magnetic by subtracting the WMM2025 magnetic declination at the given position. The result is normalized to [0, 360).',
|
|
519
|
+
inputSchema: {
|
|
520
|
+
trueBearingDeg: z.number().describe('True bearing or heading in degrees.'),
|
|
521
|
+
lat: latFragment,
|
|
522
|
+
lon: lonFragment,
|
|
523
|
+
altitudeFt: z.number().optional().describe('Optional altitude in feet MSL (default 0).'),
|
|
524
|
+
dateUtc: z
|
|
525
|
+
.string()
|
|
526
|
+
.optional()
|
|
527
|
+
.describe('Optional ISO 8601 UTC date for the lookup. Defaults to the current date.'),
|
|
528
|
+
},
|
|
529
|
+
}, ({ trueBearingDeg, lat, lon, altitudeFt, dateUtc }) => {
|
|
530
|
+
const built = buildMagneticOptions(altitudeFt, dateUtc);
|
|
531
|
+
if (!built.ok) {
|
|
532
|
+
return {
|
|
533
|
+
content: [{ type: 'text', text: built.message }],
|
|
534
|
+
structuredContent: { magneticBearingDeg: null },
|
|
535
|
+
isError: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const magneticBearingDeg = magnetic.trueToMagnetic(trueBearingDeg, lat, lon, built.options);
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: 'text', text: `${magneticBearingDeg.toFixed(1)} deg magnetic` }],
|
|
541
|
+
structuredContent: { magneticBearingDeg },
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
server.registerTool('convert_magnetic_to_true_bearing', {
|
|
545
|
+
title: 'Convert magnetic bearing to true',
|
|
546
|
+
description: 'Converts a magnetic bearing or heading to true by adding the WMM2025 magnetic declination at the given position. The result is normalized to [0, 360).',
|
|
547
|
+
inputSchema: {
|
|
548
|
+
magneticBearingDeg: z.number().describe('Magnetic bearing or heading in degrees.'),
|
|
549
|
+
lat: latFragment,
|
|
550
|
+
lon: lonFragment,
|
|
551
|
+
altitudeFt: z.number().optional().describe('Optional altitude in feet MSL (default 0).'),
|
|
552
|
+
dateUtc: z
|
|
553
|
+
.string()
|
|
554
|
+
.optional()
|
|
555
|
+
.describe('Optional ISO 8601 UTC date for the lookup. Defaults to the current date.'),
|
|
556
|
+
},
|
|
557
|
+
}, ({ magneticBearingDeg, lat, lon, altitudeFt, dateUtc }) => {
|
|
558
|
+
const built = buildMagneticOptions(altitudeFt, dateUtc);
|
|
559
|
+
if (!built.ok) {
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: 'text', text: built.message }],
|
|
562
|
+
structuredContent: { trueBearingDeg: null },
|
|
563
|
+
isError: true,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const trueBearingDeg = magnetic.magneticToTrue(magneticBearingDeg, lat, lon, built.options);
|
|
567
|
+
return {
|
|
568
|
+
content: [{ type: 'text', text: `${trueBearingDeg.toFixed(1)} deg true` }],
|
|
569
|
+
structuredContent: { trueBearingDeg },
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// Planning (fuel, PNR, ETP)
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
server.registerTool('compute_fuel_required', {
|
|
576
|
+
title: 'Compute fuel required for a leg',
|
|
577
|
+
description: 'Computes the fuel required for a flight leg given distance, groundspeed, and fuel burn rate. The result is in the same unit as fuelBurnPerHr (gallons, liters, pounds, etc.) - the function is unit-agnostic for fuel quantity.',
|
|
578
|
+
inputSchema: {
|
|
579
|
+
distanceNm: z.number().positive().describe('Leg distance in nautical miles.'),
|
|
580
|
+
groundSpeedKt: z.number().positive().describe('Groundspeed in knots.'),
|
|
581
|
+
fuelBurnPerHr: z
|
|
582
|
+
.number()
|
|
583
|
+
.positive()
|
|
584
|
+
.describe('Fuel burn rate per hour (any consistent unit: gph, lph, pph, etc.).'),
|
|
585
|
+
},
|
|
586
|
+
}, ({ distanceNm, groundSpeedKt, fuelBurnPerHr }) => {
|
|
587
|
+
const fuelRequired = planning.fuelRequired(distanceNm, groundSpeedKt, fuelBurnPerHr);
|
|
588
|
+
return {
|
|
589
|
+
content: [{ type: 'text', text: fuelRequired.toFixed(2) }],
|
|
590
|
+
structuredContent: { fuelRequired },
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
server.registerTool('compute_point_of_no_return', {
|
|
594
|
+
title: 'Compute point of no return (PNR)',
|
|
595
|
+
description: 'Computes the point of no return: the farthest distance from departure beyond which the aircraft cannot return with the fuel remaining. Accepts separate outbound and return groundspeeds so the consumer can account for wind. Fuel quantity units are arbitrary as long as fuelAvailable and fuelBurnPerHr share the same unit.',
|
|
596
|
+
inputSchema: {
|
|
597
|
+
fuelAvailable: z.number().positive().describe('Fuel on board (any consistent unit).'),
|
|
598
|
+
fuelBurnPerHr: z
|
|
599
|
+
.number()
|
|
600
|
+
.positive()
|
|
601
|
+
.describe('Fuel burn rate per hour (same unit as fuelAvailable).'),
|
|
602
|
+
groundSpeedOutKt: z.number().positive().describe('Outbound groundspeed in knots.'),
|
|
603
|
+
groundSpeedBackKt: z.number().positive().describe('Return groundspeed in knots.'),
|
|
604
|
+
},
|
|
605
|
+
}, ({ fuelAvailable, fuelBurnPerHr, groundSpeedOutKt, groundSpeedBackKt }) => {
|
|
606
|
+
const { distanceNm, timeHrs } = planning.pointOfNoReturn(fuelAvailable, fuelBurnPerHr, groundSpeedOutKt, groundSpeedBackKt);
|
|
607
|
+
return {
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: 'text',
|
|
611
|
+
text: `${distanceNm.toFixed(1)} nm at ${timeHrs.toFixed(2)} hrs`,
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
structuredContent: { distanceNm, timeHrs },
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
server.registerTool('compute_equal_time_point', {
|
|
618
|
+
title: 'Compute equal-time point (ETP)',
|
|
619
|
+
description: 'Computes the equal-time point: the point along a route where it takes the same time to continue to the destination as to return to the departure point. Accepts separate continuing and returning groundspeeds so the consumer can account for wind.',
|
|
620
|
+
inputSchema: {
|
|
621
|
+
totalDistanceNm: z.number().positive().describe('Total route distance in nautical miles.'),
|
|
622
|
+
groundSpeedOutKt: z
|
|
623
|
+
.number()
|
|
624
|
+
.positive()
|
|
625
|
+
.describe('Groundspeed continuing toward the destination in knots.'),
|
|
626
|
+
groundSpeedBackKt: z
|
|
627
|
+
.number()
|
|
628
|
+
.positive()
|
|
629
|
+
.describe('Groundspeed returning toward the departure in knots.'),
|
|
630
|
+
},
|
|
631
|
+
}, ({ totalDistanceNm, groundSpeedOutKt, groundSpeedBackKt }) => {
|
|
632
|
+
const { distanceNm, timeHrs } = planning.equalTimePoint(totalDistanceNm, groundSpeedOutKt, groundSpeedBackKt);
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: 'text',
|
|
637
|
+
text: `${distanceNm.toFixed(1)} nm at ${timeHrs.toFixed(2)} hrs`,
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
structuredContent: { distanceNm, timeHrs },
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* MCP tool module wrapping `@squawk/flightplan` route-string parsing and
|
|
4
|
+
* great-circle distance computation. The flightplan resolver composes the
|
|
5
|
+
* shared airport, navaid, fix, airway, and procedure resolvers from
|
|
6
|
+
* {@link ../resolvers.js}.
|
|
7
|
+
*/
|
|
8
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
/**
|
|
10
|
+
* Registers flight plan parsing and route distance tools on the given MCP
|
|
11
|
+
* server. The flightplan resolver is built once at registration time and
|
|
12
|
+
* shares the bundled NASR data via the resolver singletons.
|
|
13
|
+
*
|
|
14
|
+
* @param server - The MCP server instance to register tools on.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerFlightplanTools(server: McpServer): void;
|
|
17
|
+
//# sourceMappingURL=flightplan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flightplan.d.ts","sourceRoot":"","sources":["../../src/tools/flightplan.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAWzE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4D/D"}
|