@squawk/mcp 0.9.6 → 0.11.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 CHANGED
@@ -98,7 +98,7 @@ version explicitly in the client config:
98
98
  "mcpServers": {
99
99
  "squawk": {
100
100
  "command": "npx",
101
- "args": ["-y", "@squawk/mcp@0.9.6"]
101
+ "args": ["-y", "@squawk/mcp@0.11.0"]
102
102
  }
103
103
  }
104
104
  }
@@ -164,7 +164,7 @@ Pinning works the same way:
164
164
  "mcpServers": {
165
165
  "squawk": {
166
166
  "command": "npx",
167
- "args": ["-y", "-p", "@squawk/icao-registry-data@0.8.10", "@squawk/mcp@0.9.6"]
167
+ "args": ["-y", "-p", "@squawk/icao-registry-data@0.8.10", "@squawk/mcp@0.11.0"]
168
168
  }
169
169
  }
170
170
  }
@@ -249,7 +249,7 @@ directly.
249
249
  | `get_airport_by_faa_id` | Look up an airport by FAA identifier |
250
250
  | `get_airport_by_icao` | Look up an airport by ICAO code |
251
251
  | `find_nearest_airports` | Find airports nearest a position with optional facility-type and runway-length filters |
252
- | `search_airports` | Substring search by airport name or city |
252
+ | `search_airports` | Fuzzy search by FAA identifier, name, or city |
253
253
 
254
254
  ### Airspace (`@squawk/airspace` + `@squawk/airspace-data`)
255
255
 
@@ -259,23 +259,26 @@ directly.
259
259
  | `get_airspace_for_airport` | Class B/C/D/E2 surface-area sectors associated with an airport, with full polygon boundaries |
260
260
  | `find_artcc_for_position` | US ARTCC features containing a given position and altitude (typically one feature; multiple for oceanic CTA+FIR overlaps or stratum boundaries) |
261
261
  | `find_artcc_by_identifier` | All ARTCC features for a 3-letter center code (e.g. "ZNY"), optionally narrowed to a single stratum, with full polygon boundaries |
262
+ | `search_airspace` | Fuzzy search by identifier or name (boundary summarized as a vertex count) |
262
263
 
263
264
  ### Navaids (`@squawk/navaids` + `@squawk/navaid-data`)
264
265
 
265
- | Tool | Purpose |
266
- | --------------------------- | ----------------------------------------------- |
267
- | `get_navaid_by_ident` | Look up navaids by identifier |
268
- | `find_navaids_by_frequency` | Find navaids tuned to a given MHz/kHz frequency |
269
- | `find_nearest_navaids` | Find navaids nearest a position |
270
- | `search_navaids` | Substring search by name or identifier |
266
+ | Tool | Purpose |
267
+ | --------------------------------- | -------------------------------------------------------- |
268
+ | `get_navaid_by_ident` | Look up navaids by identifier |
269
+ | `get_navaid_by_ident_at_position` | Look up the navaid sharing an identifier nearest a point |
270
+ | `find_navaids_by_frequency` | Find navaids tuned to a given MHz/kHz frequency |
271
+ | `find_nearest_navaids` | Find navaids nearest a position |
272
+ | `search_navaids` | Fuzzy search by name or identifier |
271
273
 
272
274
  ### Fixes (`@squawk/fixes` + `@squawk/fix-data`)
273
275
 
274
- | Tool | Purpose |
275
- | -------------------- | ------------------------------ |
276
- | `get_fix_by_ident` | Look up fixes by identifier |
277
- | `find_nearest_fixes` | Find fixes nearest a position |
278
- | `search_fixes` | Substring search by identifier |
276
+ | Tool | Purpose |
277
+ | ------------------------------ | ----------------------------------------------------- |
278
+ | `get_fix_by_ident` | Look up fixes by identifier |
279
+ | `get_fix_by_ident_at_position` | Look up the fix sharing an identifier nearest a point |
280
+ | `find_nearest_fixes` | Find fixes nearest a position |
281
+ | `search_fixes` | Fuzzy search by identifier |
279
282
 
280
283
  ### Airways (`@squawk/airways` + `@squawk/airway-data`)
281
284
 
@@ -284,7 +287,7 @@ directly.
284
287
  | `get_airway_by_designation` | Look up airways by designation |
285
288
  | `expand_airway_segment` | Expand an airway between an entry fix and an exit fix |
286
289
  | `find_airways_by_fix` | Reverse lookup: airways that pass through a given fix |
287
- | `search_airways` | Substring search by designation |
290
+ | `search_airways` | Fuzzy search by designation |
288
291
 
289
292
  ### Procedures (`@squawk/procedures` + `@squawk/procedure-data`)
290
293
 
@@ -298,7 +301,8 @@ Covers SIDs, STARs, and Instrument Approach Procedures (IAPs) from FAA CIFP.
298
301
  | `find_procedures_by_airport_and_runway` | Procedures at an airport serving a specific runway (IAP runway match or RW\* transition) |
299
302
  | `find_approaches_by_type` | Every IAP of a given approach classification (ILS, RNAV, VOR, etc.) |
300
303
  | `expand_procedure` | Expand a procedure into its leg sequence (with optional transition merge) |
301
- | `search_procedures` | Substring search by name or identifier, optionally filtered by procedure or approach type |
304
+ | `get_procedure_geometry` | Ordered drawable fix points and a GeoJSON LineString for a procedure |
305
+ | `search_procedures` | Fuzzy search by name or identifier, optionally filtered by procedure or approach type |
302
306
 
303
307
  ### ICAO aircraft registry (`@squawk/icao-registry` + `@squawk/icao-registry-data`)
304
308
 
@@ -342,6 +346,8 @@ lookup so sessions that never need it do not pay the cost.
342
346
  | ------------------------ | ---------------------------------------------------------- |
343
347
  | `parse_flightplan_route` | Parse an ICAO Item 15 route into structured route elements |
344
348
  | `compute_route_distance` | Total great-circle route distance with optional ETE |
349
+ | `get_route_geometry` | Ordered drawable points and a GeoJSON LineString |
350
+ | `get_route_timing` | Per-leg wind-corrected timing, ETE, and fuel/endurance |
345
351
 
346
352
  ### Flight computer (`@squawk/flight-math`)
347
353
 
@@ -1 +1 @@
1
- {"version":3,"file":"airports.d.ts","sourceRoot":"","sources":["../../src/tools/airports.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkBzE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6I5D"}
1
+ {"version":3,"file":"airports.d.ts","sourceRoot":"","sources":["../../src/tools/airports.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkBzE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2J5D"}
@@ -111,9 +111,12 @@ export function registerAirportTools(server) {
111
111
  });
112
112
  server.registerTool('search_airports', {
113
113
  title: 'Search airports by name or city',
114
- description: 'Searches US airports using case-insensitive substring matching on airport name and city. Returns airports in alphabetical order by name. Use this when the user knows a partial name but not the identifier.',
114
+ description: 'Fuzzy-searches US airports across FAA identifier, name, and city. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match) and the matched field; results are returned best-match first. Use this when the user knows a partial name but not the identifier.',
115
115
  inputSchema: {
116
- text: z.string().min(1).describe('Substring to match against airport name or city.'),
116
+ text: z
117
+ .string()
118
+ .min(1)
119
+ .describe('Search text, fuzzily matched against airport FAA identifier, name, and city.'),
117
120
  limit: z
118
121
  .number()
119
122
  .int()
@@ -124,8 +127,14 @@ export function registerAirportTools(server) {
124
127
  .array(z.enum(FACILITY_TYPE_VALUES))
125
128
  .optional()
126
129
  .describe('Restrict results to these facility types. Omit to include all types.'),
130
+ minScore: z
131
+ .number()
132
+ .min(0)
133
+ .max(1)
134
+ .optional()
135
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
127
136
  },
128
- }, ({ text, limit, facilityTypes }) => {
137
+ }, ({ text, limit, facilityTypes, minScore }) => {
129
138
  const query = { text };
130
139
  if (limit !== undefined) {
131
140
  query.limit = limit;
@@ -133,6 +142,9 @@ export function registerAirportTools(server) {
133
142
  if (facilityTypes !== undefined) {
134
143
  query.types = new Set(facilityTypes);
135
144
  }
145
+ if (minScore !== undefined) {
146
+ query.minScore = minScore;
147
+ }
136
148
  const results = resolver.search(query);
137
149
  return {
138
150
  content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
@@ -1 +1 @@
1
- {"version":3,"file":"airspace.d.ts","sourceRoot":"","sources":["../../src/tools/airspace.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAmFzE;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmI7D"}
1
+ {"version":3,"file":"airspace.d.ts","sourceRoot":"","sources":["../../src/tools/airspace.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAmFzE;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA0L7D"}
@@ -165,4 +165,51 @@ export function registerAirspaceTools(server) {
165
165
  structuredContent: { features },
166
166
  };
167
167
  });
168
+ server.registerTool('search_airspace', {
169
+ title: 'Search airspace by name or identifier',
170
+ description: 'Fuzzy-searches US airspace features across identifier and name. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match) and the matched field; results are returned best-match first. Boundary geometry is summarized as a vertex count to keep responses compact.',
171
+ inputSchema: {
172
+ text: z
173
+ .string()
174
+ .min(1)
175
+ .describe('Search text, fuzzily matched against airspace identifier and name.'),
176
+ airspaceTypes: z
177
+ .array(z.enum(AIRSPACE_TYPE_VALUES))
178
+ .optional()
179
+ .describe('Restrict results to these airspace types. Omit to include all types.'),
180
+ limit: z
181
+ .number()
182
+ .int()
183
+ .positive()
184
+ .optional()
185
+ .describe('Maximum number of results to return. Defaults to 20.'),
186
+ minScore: z
187
+ .number()
188
+ .min(0)
189
+ .max(1)
190
+ .optional()
191
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
192
+ },
193
+ }, ({ text, airspaceTypes, limit, minScore }) => {
194
+ const query = { text };
195
+ if (airspaceTypes !== undefined) {
196
+ query.types = new Set(airspaceTypes);
197
+ }
198
+ if (limit !== undefined) {
199
+ query.limit = limit;
200
+ }
201
+ if (minScore !== undefined) {
202
+ query.minScore = minScore;
203
+ }
204
+ const results = airspaceResolver.search(query).map((result) => ({
205
+ feature: summarizeFeature(result.feature),
206
+ score: result.score,
207
+ matchedField: result.matchedField,
208
+ ranges: result.ranges,
209
+ }));
210
+ return {
211
+ content: [{ type: 'text', text: JSON.stringify({ results }, null, 2) }],
212
+ structuredContent: { results },
213
+ };
214
+ });
168
215
  }
@@ -1 +1 @@
1
- {"version":3,"file":"airways.d.ts","sourceRoot":"","sources":["../../src/tools/airways.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAwBzE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6G3D"}
1
+ {"version":3,"file":"airways.d.ts","sourceRoot":"","sources":["../../src/tools/airways.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAwBzE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2H3D"}
@@ -84,9 +84,12 @@ export function registerAirwayTools(server) {
84
84
  });
85
85
  server.registerTool('search_airways', {
86
86
  title: 'Search airways by designation',
87
- description: 'Searches US airways by case-insensitive substring matching against the airway designation. Results are returned in alphabetical order by designation.',
87
+ description: 'Fuzzy-searches US airways by designation. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match); results are returned best-match first.',
88
88
  inputSchema: {
89
- text: z.string().min(1).describe('Substring to match against the airway designation.'),
89
+ text: z
90
+ .string()
91
+ .min(1)
92
+ .describe('Search text, fuzzily matched against the airway designation.'),
90
93
  airwayTypes: z
91
94
  .array(z.enum(AIRWAY_TYPE_VALUES))
92
95
  .optional()
@@ -97,8 +100,14 @@ export function registerAirwayTools(server) {
97
100
  .positive()
98
101
  .optional()
99
102
  .describe('Maximum number of results to return. Defaults to 20.'),
103
+ minScore: z
104
+ .number()
105
+ .min(0)
106
+ .max(1)
107
+ .optional()
108
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
100
109
  },
101
- }, ({ text, airwayTypes, limit }) => {
110
+ }, ({ text, airwayTypes, limit, minScore }) => {
102
111
  const query = { text };
103
112
  if (airwayTypes !== undefined) {
104
113
  query.types = new Set(airwayTypes);
@@ -106,6 +115,9 @@ export function registerAirwayTools(server) {
106
115
  if (limit !== undefined) {
107
116
  query.limit = limit;
108
117
  }
118
+ if (minScore !== undefined) {
119
+ query.minScore = minScore;
120
+ }
109
121
  const airways = airwayResolver.search(query);
110
122
  return {
111
123
  content: [{ type: 'text', text: JSON.stringify(airways, null, 2) }],
@@ -1 +1 @@
1
- {"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../src/tools/fixes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoBzE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAsGxD"}
1
+ {"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../src/tools/fixes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoBzE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAwJxD"}
@@ -36,6 +36,34 @@ export function registerFixTools(server) {
36
36
  structuredContent: { fixes },
37
37
  };
38
38
  });
39
+ server.registerTool('get_fix_by_ident_at_position', {
40
+ title: 'Get fix by identifier nearest a position',
41
+ description: 'Looks up the single US fix/waypoint sharing the given identifier that lies nearest to a geographic position. The same fix identifier can be published in more than one ICAO region; this disambiguates the collision by proximity to a known point such as a map-click location or an adjacent route waypoint. When toleranceNm is provided, matches farther than that distance are excluded and the result is null; when omitted, the nearest match wins regardless of distance. Returns null when no fix carries the identifier.',
42
+ inputSchema: {
43
+ ident: z.string().min(1).describe('Fix identifier (case-insensitive).'),
44
+ lat: z
45
+ .number()
46
+ .min(-90)
47
+ .max(90)
48
+ .describe('Latitude of the reference position in decimal degrees (WGS84).'),
49
+ lon: z
50
+ .number()
51
+ .min(-180)
52
+ .max(180)
53
+ .describe('Longitude of the reference position in decimal degrees (WGS84).'),
54
+ toleranceNm: z
55
+ .number()
56
+ .positive()
57
+ .optional()
58
+ .describe('Maximum great-circle distance in nautical miles. Omit to let the nearest match win regardless of distance.'),
59
+ },
60
+ }, ({ ident, lat, lon, toleranceNm }) => {
61
+ const fix = fixResolver.byIdentAtPosition(ident, lat, lon, toleranceNm) ?? null;
62
+ return {
63
+ content: [{ type: 'text', text: JSON.stringify(fix, null, 2) }],
64
+ structuredContent: { fix },
65
+ };
66
+ });
39
67
  server.registerTool('find_nearest_fixes', {
40
68
  title: 'Find nearest fixes',
41
69
  description: 'Finds US fixes/waypoints near a geographic position, sorted by great-circle distance in nautical miles.',
@@ -77,9 +105,12 @@ export function registerFixTools(server) {
77
105
  });
78
106
  server.registerTool('search_fixes', {
79
107
  title: 'Search fixes by identifier',
80
- description: 'Searches US fixes/waypoints by case-insensitive substring matching against the fix identifier. Results are returned in alphabetical order by identifier.',
108
+ description: 'Fuzzy-searches US fixes/waypoints by identifier. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match); results are returned best-match first.',
81
109
  inputSchema: {
82
- text: z.string().min(1).describe('Substring to match against the fix identifier.'),
110
+ text: z
111
+ .string()
112
+ .min(1)
113
+ .describe('Search text, fuzzily matched against the fix identifier.'),
83
114
  useCodes: z
84
115
  .array(z.enum(FIX_USE_CODE_VALUES))
85
116
  .optional()
@@ -90,8 +121,14 @@ export function registerFixTools(server) {
90
121
  .positive()
91
122
  .optional()
92
123
  .describe('Maximum number of results to return. Defaults to 20.'),
124
+ minScore: z
125
+ .number()
126
+ .min(0)
127
+ .max(1)
128
+ .optional()
129
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
93
130
  },
94
- }, ({ text, useCodes, limit }) => {
131
+ }, ({ text, useCodes, limit, minScore }) => {
95
132
  const query = { text };
96
133
  if (useCodes !== undefined) {
97
134
  query.useCodes = new Set(useCodes);
@@ -99,6 +136,9 @@ export function registerFixTools(server) {
99
136
  if (limit !== undefined) {
100
137
  query.limit = limit;
101
138
  }
139
+ if (minScore !== undefined) {
140
+ query.minScore = minScore;
141
+ }
102
142
  const fixes = fixResolver.search(query);
103
143
  return {
104
144
  content: [{ type: 'text', text: JSON.stringify(fixes, null, 2) }],
@@ -1 +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;AAazE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4D/D"}
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;AAmBzE;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAgJ/D"}
@@ -6,7 +6,7 @@
6
6
  * package's `resolvers` module.
7
7
  */
8
8
  import { z } from 'zod';
9
- import { computeRouteDistance, createFlightplanResolver } from '@squawk/flightplan';
9
+ import { computeRouteDistance, computeRouteTiming, createFlightplanResolver, extractRoutePoints, routeToLineString, } from '@squawk/flightplan';
10
10
  import { airportResolver, airwayResolver, fixResolver, navaidResolver, procedureResolver, } from '../resolvers.js';
11
11
  /**
12
12
  * Registers flight plan parsing and route distance tools on the given MCP
@@ -41,7 +41,7 @@ export function registerFlightplanTools(server) {
41
41
  });
42
42
  server.registerTool('compute_route_distance', {
43
43
  title: 'Compute route distance and ETE',
44
- description: 'Parses a flight plan route string and computes the total great-circle distance in nautical miles, the ordered list of legs with cumulative distance, and (when groundSpeedKt is supplied) the estimated time enroute in hours. Uses FAA-published per-segment distances on airway segments when available; otherwise falls back to great-circle computation. Unresolved tokens are surfaced separately so the caller can decide whether to trust the total.',
44
+ description: 'Parses a flight plan route string and computes the total great-circle distance in nautical miles, the ordered list of legs with cumulative distance, and (when groundSpeedKt is supplied) the estimated time enroute in hours. Each leg carries the from/to point labels and their latitude/longitude in decimal degrees, so the result doubles as a drawable leg-by-leg geometry. Uses FAA-published per-segment distances on airway segments when available; otherwise falls back to great-circle computation. Unresolved tokens are surfaced separately so the caller can decide whether to trust the total. To get the route as a single ordered point sequence or a GeoJSON LineString for map rendering, use get_route_geometry instead.',
45
45
  inputSchema: {
46
46
  routeString: z
47
47
  .string()
@@ -61,4 +61,70 @@ export function registerFlightplanTools(server) {
61
61
  structuredContent: { result },
62
62
  };
63
63
  });
64
+ server.registerTool('get_route_geometry', {
65
+ title: 'Get drawable route geometry',
66
+ description: 'Parses a flight plan route string and returns its drawable geometry: the ordered sequence of geographic points (each with a label and latitude/longitude in decimal degrees) and a GeoJSON LineString ready to render as a polyline on a map. Airway and SID/STAR segments are expanded into their constituent fixes, and consecutive duplicate points (e.g. an airway entry fix that matches the preceding waypoint) are suppressed. LineString coordinates follow the GeoJSON [lon, lat] ordering. The lineString field is omitted when the route yields fewer than two drawable points, since a LineString requires at least two positions. Use compute_route_distance instead when you need leg distances or estimated time enroute.',
67
+ inputSchema: {
68
+ routeString: z
69
+ .string()
70
+ .min(1)
71
+ .describe('Whitespace-separated route string in ICAO Item 15 conventions.'),
72
+ },
73
+ }, ({ routeString }) => {
74
+ const route = resolver.parse(routeString);
75
+ const points = extractRoutePoints(route);
76
+ const lineString = routeToLineString(route);
77
+ const result = { points, lineString };
78
+ return {
79
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
80
+ structuredContent: result,
81
+ };
82
+ });
83
+ server.registerTool('get_route_timing', {
84
+ title: 'Compute wind-corrected route timing',
85
+ description: 'Parses a flight plan route string and computes per-leg, wind-corrected timing. For each leg it solves the wind triangle from the leg true course, the given true airspeed, and a uniform wind, yielding true heading, wind correction angle, and ground speed, then estimated time enroute (and fuel burn when fuelBurnPerHr is given). Supply a uniform wind via the optional wind object (direction the wind blows FROM in degrees true, and speed in knots) applied to every leg; omit it to time the route as calm, where ground speed equals true airspeed. When fuelBurnPerHr and fuelAvailable are both given, the result also reports endurance in hours and whether fuel is sufficient for the total time enroute. A leg whose ground speed is not positive (a pure headwind equal to true airspeed) is left untimed, which makes the route ETE and fuel totals null. Use compute_route_distance for plain distance or a single constant-ground-speed ETE without wind correction.',
86
+ inputSchema: {
87
+ routeString: z
88
+ .string()
89
+ .min(1)
90
+ .describe('Whitespace-separated route string in ICAO Item 15 conventions.'),
91
+ trueAirspeedKt: z
92
+ .number()
93
+ .positive()
94
+ .describe('True airspeed in knots flown on every leg.'),
95
+ wind: z
96
+ .object({
97
+ directionDeg: z
98
+ .number()
99
+ .min(0)
100
+ .max(360)
101
+ .describe('Direction the wind blows FROM in degrees true (0-360).'),
102
+ speedKt: z.number().nonnegative().describe('Wind speed in knots.'),
103
+ })
104
+ .optional()
105
+ .describe('Uniform wind applied to every leg. Omit to time the route as calm (ground speed equals true airspeed).'),
106
+ fuelBurnPerHr: z
107
+ .number()
108
+ .positive()
109
+ .optional()
110
+ .describe('Optional fuel burn rate per hour in any consistent unit. When given, per-leg and total fuel are computed.'),
111
+ fuelAvailable: z
112
+ .number()
113
+ .positive()
114
+ .optional()
115
+ .describe('Optional fuel on board in the same unit as fuelBurnPerHr. With the burn rate, enables endurance and fuel-sufficiency reporting.'),
116
+ },
117
+ }, ({ routeString, trueAirspeedKt, wind, fuelBurnPerHr, fuelAvailable }) => {
118
+ const route = resolver.parse(routeString);
119
+ const result = computeRouteTiming(route, {
120
+ trueAirspeedKt,
121
+ ...(wind !== undefined ? { windProvider: () => wind } : {}),
122
+ ...(fuelBurnPerHr !== undefined ? { fuelBurnPerHr } : {}),
123
+ ...(fuelAvailable !== undefined ? { fuelAvailable } : {}),
124
+ });
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
127
+ structuredContent: { result },
128
+ };
129
+ });
64
130
  }
@@ -1 +1 @@
1
- {"version":3,"file":"navaids.d.ts","sourceRoot":"","sources":["../../src/tools/navaids.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2I3D"}
1
+ {"version":3,"file":"navaids.d.ts","sourceRoot":"","sources":["../../src/tools/navaids.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA6L3D"}
@@ -38,6 +38,34 @@ export function registerNavaidTools(server) {
38
38
  structuredContent: { navaids },
39
39
  };
40
40
  });
41
+ server.registerTool('get_navaid_by_ident_at_position', {
42
+ title: 'Get navaid by identifier nearest a position',
43
+ description: 'Looks up the single US navaid sharing the given identifier that lies nearest to a geographic position. Multiple navaids can publish the same identifier (a co-located NDB and VOR/DME, or two distant stations reusing a code); this disambiguates them by proximity to a known point such as a map-click location or an adjacent route waypoint. When toleranceNm is provided, matches farther than that distance are excluded and the result is null; when omitted, the nearest match wins regardless of distance. Returns null when no navaid carries the identifier.',
44
+ inputSchema: {
45
+ ident: z.string().min(1).describe('Navaid identifier (case-insensitive).'),
46
+ lat: z
47
+ .number()
48
+ .min(-90)
49
+ .max(90)
50
+ .describe('Latitude of the reference position in decimal degrees (WGS84).'),
51
+ lon: z
52
+ .number()
53
+ .min(-180)
54
+ .max(180)
55
+ .describe('Longitude of the reference position in decimal degrees (WGS84).'),
56
+ toleranceNm: z
57
+ .number()
58
+ .positive()
59
+ .optional()
60
+ .describe('Maximum great-circle distance in nautical miles. Omit to let the nearest match win regardless of distance.'),
61
+ },
62
+ }, ({ ident, lat, lon, toleranceNm }) => {
63
+ const navaid = navaidResolver.byIdentAtPosition(ident, lat, lon, toleranceNm) ?? null;
64
+ return {
65
+ content: [{ type: 'text', text: JSON.stringify(navaid, null, 2) }],
66
+ structuredContent: { navaid },
67
+ };
68
+ });
41
69
  server.registerTool('find_navaids_by_frequency', {
42
70
  title: 'Find navaids by frequency',
43
71
  description: 'Finds US navaids tuned to a given frequency. VOR-family navaids (VOR, VORTAC, VOR/DME, TACAN, DME, VOT) use MHz; NDB-family navaids (NDB, NDB/DME, MARINE_NDB) use kHz. Filter by navaid types when needed to disambiguate the unit.',
@@ -112,9 +140,12 @@ export function registerNavaidTools(server) {
112
140
  });
113
141
  server.registerTool('search_navaids', {
114
142
  title: 'Search navaids by name or identifier',
115
- description: 'Searches US navaids using case-insensitive substring matching against the navaid name and identifier. Results are returned in alphabetical order by name.',
143
+ description: 'Fuzzy-searches US navaids across identifier and name. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match) and the matched field; results are returned best-match first.',
116
144
  inputSchema: {
117
- text: z.string().min(1).describe('Substring to match against navaid name or identifier.'),
145
+ text: z
146
+ .string()
147
+ .min(1)
148
+ .describe('Search text, fuzzily matched against navaid identifier and name.'),
118
149
  navaidTypes: z
119
150
  .array(z.enum(NAVAID_TYPE_VALUES))
120
151
  .optional()
@@ -125,8 +156,14 @@ export function registerNavaidTools(server) {
125
156
  .positive()
126
157
  .optional()
127
158
  .describe('Maximum number of results to return. Defaults to 20.'),
159
+ minScore: z
160
+ .number()
161
+ .min(0)
162
+ .max(1)
163
+ .optional()
164
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
128
165
  },
129
- }, ({ text, navaidTypes, limit }) => {
166
+ }, ({ text, navaidTypes, limit, minScore }) => {
130
167
  const query = { text };
131
168
  if (navaidTypes !== undefined) {
132
169
  query.types = new Set(navaidTypes);
@@ -134,6 +171,9 @@ export function registerNavaidTools(server) {
134
171
  if (limit !== undefined) {
135
172
  query.limit = limit;
136
173
  }
174
+ if (minScore !== undefined) {
175
+ query.minScore = minScore;
176
+ }
137
177
  const navaids = navaidResolver.search(query);
138
178
  return {
139
179
  content: [{ type: 'text', text: JSON.stringify(navaids, null, 2) }],
@@ -1 +1 @@
1
- {"version":3,"file":"procedures.d.ts","sourceRoot":"","sources":["../../src/tools/procedures.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgCzE;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA2M9D"}
1
+ {"version":3,"file":"procedures.d.ts","sourceRoot":"","sources":["../../src/tools/procedures.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiCzE;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAgQ9D"}
@@ -5,6 +5,7 @@
5
5
  * `@squawk/procedure-data`.
6
6
  */
7
7
  import { z } from 'zod';
8
+ import { expansionToLineString, extractLegPoints } from '@squawk/procedures';
8
9
  import { procedureResolver } from '../resolvers.js';
9
10
  /** All {@link ProcedureType} values, used for input validation. */
10
11
  const PROCEDURE_TYPE_VALUES = ['SID', 'STAR', 'IAP'];
@@ -127,7 +128,7 @@ export function registerProcedureTools(server) {
127
128
  });
128
129
  server.registerTool('expand_procedure', {
129
130
  title: 'Expand a procedure into an ordered leg sequence',
130
- description: "Expands a procedure at an airport into an ordered leg sequence. Without a transition name, returns the procedure's common route. With a transition name, merges the named transition's legs with the common route in flying order (SID: common then transition for enroute exits, transition then common for runway transitions; STAR: transition then common for enroute entries; IAP: approach transition then final approach segment). Returns null when the airport, procedure, or transition is not found.",
131
+ description: "Expands a procedure at an airport into an ordered leg sequence. Without a transition name, returns the procedure's common route. With a transition name, merges the named transition's legs with the common route in flying order (SID: common then transition for enroute exits, transition then common for runway transitions; STAR: transition then common for enroute entries; IAP: approach transition then final approach segment). Returns null when the airport, procedure, or transition is not found. To get just the drawable point sequence or a GeoJSON LineString for map rendering, use get_procedure_geometry instead.",
131
132
  inputSchema: {
132
133
  airportId: z.string().min(1).describe('Airport identifier (case-insensitive).'),
133
134
  identifier: z.string().min(1).describe('CIFP procedure identifier (case-insensitive).'),
@@ -155,14 +156,50 @@ export function registerProcedureTools(server) {
155
156
  structuredContent: { expansion },
156
157
  };
157
158
  });
159
+ server.registerTool('get_procedure_geometry', {
160
+ title: 'Get drawable procedure geometry',
161
+ description: 'Expands a procedure at an airport (optionally merging a named transition, exactly like expand_procedure) and returns its drawable geometry: the ordered sequence of fix points (each with a label and latitude/longitude in decimal degrees) and a GeoJSON LineString ready to render as a polyline on a map. Only legs that terminate at a known fix contribute a point; ARINC 424 legs that end at an altitude, DME distance, radial, intercept, or manual event (CA, FA, VA, CD, FD, VD, CR, VR, CI, VI, FM, VM, and the HA/HM holds) carry no coordinate and are skipped, so the line breaks across them and is an approximation rather than a precise flyable track. Consecutive duplicate points are suppressed. LineString coordinates follow the GeoJSON [lon, lat] ordering. The lineString field is omitted when fewer than two fix points are drawable. Returns null when the airport, procedure, or transition is not found. Use expand_procedure instead when you need the full leg fields (constraints, courses, path terminators) rather than just geometry.',
162
+ inputSchema: {
163
+ airportId: z
164
+ .string()
165
+ .min(1)
166
+ .describe('Airport identifier (ICAO or FAA, case-insensitive).'),
167
+ identifier: z.string().min(1).describe('CIFP procedure identifier (case-insensitive).'),
168
+ transitionName: z
169
+ .string()
170
+ .min(1)
171
+ .optional()
172
+ .describe('Optional transition name. Omit to use just the common route.'),
173
+ },
174
+ }, ({ airportId, identifier, transitionName }) => {
175
+ const expansion = procedureResolver.expand(airportId, identifier, transitionName);
176
+ if (expansion === undefined) {
177
+ return {
178
+ content: [
179
+ {
180
+ type: 'text',
181
+ text: `Could not expand procedure "${identifier}" at "${airportId}"${transitionName ? ` with transition "${transitionName}"` : ''}.`,
182
+ },
183
+ ],
184
+ structuredContent: { points: null, lineString: null },
185
+ };
186
+ }
187
+ const points = extractLegPoints(expansion.legs);
188
+ const lineString = expansionToLineString(expansion.legs);
189
+ const result = { points, lineString };
190
+ return {
191
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
192
+ structuredContent: result,
193
+ };
194
+ });
158
195
  server.registerTool('search_procedures', {
159
196
  title: 'Search procedures by name or identifier',
160
- description: 'Searches US instrument procedures (SIDs, STARs, IAPs) by case-insensitive substring matching against both the procedure name and identifier. Results are returned sorted by airport and identifier.',
197
+ description: 'Fuzzy-searches US instrument procedures (SIDs, STARs, IAPs) across identifier and name. Matching is case-insensitive and tolerant of prefixes, substrings, subsequences, and small typos. Each result carries a match score in [0, 1] (1 is an exact match) and the matched field; results are returned best-match first.',
161
198
  inputSchema: {
162
199
  text: z
163
200
  .string()
164
201
  .min(1)
165
- .describe('Substring to match against the procedure name or identifier.'),
202
+ .describe('Search text, fuzzily matched against the procedure identifier and name.'),
166
203
  procedureType: z
167
204
  .enum(PROCEDURE_TYPE_VALUES)
168
205
  .optional()
@@ -177,8 +214,14 @@ export function registerProcedureTools(server) {
177
214
  .positive()
178
215
  .optional()
179
216
  .describe('Maximum number of results to return. Defaults to 20.'),
217
+ minScore: z
218
+ .number()
219
+ .min(0)
220
+ .max(1)
221
+ .optional()
222
+ .describe('Minimum match score (exclusive) in [0, 1] a result must reach. Defaults to 0. Raise it to drop weak fuzzy matches.'),
180
223
  },
181
- }, ({ text, procedureType, approachType, limit }) => {
224
+ }, ({ text, procedureType, approachType, limit, minScore }) => {
182
225
  const query = { text };
183
226
  if (procedureType !== undefined) {
184
227
  query.type = procedureType;
@@ -189,6 +232,9 @@ export function registerProcedureTools(server) {
189
232
  if (limit !== undefined) {
190
233
  query.limit = limit;
191
234
  }
235
+ if (minScore !== undefined) {
236
+ query.minScore = minScore;
237
+ }
192
238
  const procedures = procedureResolver.search(query);
193
239
  return {
194
240
  content: [{ type: 'text', text: JSON.stringify(procedures, null, 2) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squawk/mcp",
3
- "version": "0.9.6",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Model Context Protocol server exposing squawk's aviation libraries as tools for LLM clients",
6
6
  "author": "Neil Cochran",
@@ -42,24 +42,24 @@
42
42
  "dependencies": {
43
43
  "@modelcontextprotocol/sdk": "^1.29.0",
44
44
  "@squawk/airport-data": "^0.7.3",
45
- "@squawk/airports": "^0.6.1",
46
- "@squawk/airspace": "^0.8.0",
45
+ "@squawk/airports": "^0.7.0",
46
+ "@squawk/airspace": "^0.9.0",
47
47
  "@squawk/airspace-data": "^0.5.2",
48
48
  "@squawk/airway-data": "^0.5.3",
49
- "@squawk/airways": "^0.4.1",
49
+ "@squawk/airways": "^0.5.0",
50
50
  "@squawk/fix-data": "^0.6.3",
51
- "@squawk/fixes": "^0.3.1",
51
+ "@squawk/fixes": "^0.5.0",
52
52
  "@squawk/flight-math": "^0.5.3",
53
- "@squawk/flightplan": "^0.5.1",
53
+ "@squawk/flightplan": "^0.6.0",
54
54
  "@squawk/geo": "^0.4.3",
55
55
  "@squawk/icao-registry": "^0.5.1",
56
56
  "@squawk/navaid-data": "^0.6.3",
57
- "@squawk/navaids": "^0.4.1",
57
+ "@squawk/navaids": "^0.6.0",
58
58
  "@squawk/notams": "^0.3.5",
59
59
  "@squawk/procedure-data": "^0.7.2",
60
- "@squawk/procedures": "^0.5.1",
60
+ "@squawk/procedures": "^0.7.0",
61
61
  "@squawk/types": "^0.8.0",
62
- "@squawk/weather": "^0.5.5",
62
+ "@squawk/weather": "^0.6.0",
63
63
  "zod": "^4.4.3"
64
64
  },
65
65
  "peerDependencies": {