Skip to content

Commit 06d458e

Browse files
SoapyREDclaude
andcommitted
chore(release): 2.5.0 — tighten loose tool-input schemas (mirror website MCP surface)
Tighten bare z.string() inputs to their valid-form unions across the tools so garbage is rejected at the schema layer while every legitimate form still validates, mirroring the website /api/mcp surface identically (FAULT 13): - unit_converter: from/to enums of the supported unit codes (+ chargeable_kg / freight_tonnes as conversion targets). - hs_code_lookup: code ^\d{2,6}$, section Roman numeral I-XXI, query >= 2. - incoterms_lookup: code 3-letter Incoterm. - uk_duty_calculator: incoterm enum (commodity/origin regexes aligned to website). - airline_lookup: iata ^[A-Za-z0-9]{2}$, icao ^[A-Za-z]{3}$ (from length checks). - unlocode_lookup: function -> function_type enum (port|airport|rail|road|icd| border); code/country regexes aligned to website. - container/uld/vehicle selectors: require >= 2 chars (kept permissive code-or-slug). No tool-count or tool-list change (still 19). Minor bump: advertised input schemas changed + the unlocode function -> function_type rename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d66d383 commit 06d458e

3 files changed

Lines changed: 35 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## 2.5.0 — 2026-06-19
4+
5+
### Changed
6+
7+
- **Loose-schema tightening across the tool inputs — garbage is now rejected at the schema layer while every legitimate input form still validates.** Several tools advertised a bare `z.string()` where the description promised a specific format, so malformed values passed validation and only failed (or silently returned nothing) downstream. Each is now constrained to exactly the forms its underlying data/logic accepts, with a clear error message, without narrowing any feature:
8+
- **`unit_converter`**`from` is now an enum of the 17 supported unit codes (weight / volume / length); `to` is those plus the freight targets `chargeable_kg` and `freight_tonnes`. Unknown units are rejected with the valid set surfaced in `tools/list`.
9+
- **`hs_code_lookup`**`code` must be 2–6 digits, `section` a Roman numeral (I–XXI), `query` ≥ 2 chars.
10+
- **`incoterms_lookup`**`code` must be a 3-letter Incoterm code (any case).
11+
- **`uk_duty_calculator`**`incoterm` is now the 11-value Incoterms enum (commodity_code `^\d{6,10}$` and origin_country `^[A-Za-z]{2}$` were already tight; regex form aligned with the website surface).
12+
- **`airline_lookup`**`iata` `^[A-Za-z0-9]{2}$`, `icao` `^[A-Za-z]{3}$` (upgraded from length checks); the 2-char IATA / 3-char ICAO / 3-digit AWB-prefix / free-form country+query union is fully preserved.
13+
- **`unlocode_lookup`** — the function filter is now **`function_type`** (an enum of `port | airport | rail | road | icd | border`), renamed from the previous bare-string `function` to match the website MCP surface and validated as an enum; `code` / `country` regexes aligned to the website (`^[A-Za-z0-9]{5}$` / `^[A-Za-z]{2}$`). **Note:** callers that passed `function` must switch to `function_type`.
14+
- **`container_lookup` / `uld_lookup` / `vehicle_lookup`** — the slug/code selector now requires ≥ 2 chars; these accept a code **or** slug from a fixed reference set, so they stay permissive (the handler returns a clear "not found" for unknown values) rather than being narrowed to a brittle enum.
15+
16+
### Notes
17+
18+
- **No tool-count or tool-list change — still 19 tools, same names.** This is a FAULT 13 fix-once-mirror-everywhere release: the website `/api/mcp` surface ([`app/api/mcp/[transport]/route.ts`](https://github.com/SoapyRED/freighttools)) was tightened to the identical schemas in the same change, so `tools/list` and input validation match across both surfaces. `serverInfo.version` reads dynamically from `package.json`, now `2.5.0`. Minor bump because the advertised input schemas changed (stricter validation + the `unlocode_lookup` `function``function_type` rename).
19+
320
## 2.4.1 — 2026-06-17
421

522
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "freightutils-mcp",
3-
"version": "2.4.1",
3+
"version": "2.5.0",
44
"mcpName": "io.github.SoapyRED/freightutils",
55
"description": "FreightUtils MCP Server — 19 free freight tools for AI agents: ADR 2025 dangerous goods (2,939 entries) with LQ/EQ check, HS 2022 codes (6,940), airlines (6,352), UN/LOCODE (116,129 locations), LDM/CBM/chargeable weight calculators, UK duty estimator, subscribe-link helper, and more.",
66
"author": "FreightUtils <contact@freightutils.com>",

src/tools.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ AWB prefixes are 3-digit codes used on air waybills to identify the issuing carr
224224

225225
schema: z.object({
226226
query: z.string().min(2, 'Query must be at least 2 characters').optional().describe('General search (name, code, prefix, or country — min 2 chars)'),
227-
iata: z.string().length(2, 'IATA code must be exactly 2 characters').optional().describe('Exact IATA code (2 chars, e.g., "EK")'),
228-
icao: z.string().length(3, 'ICAO code must be exactly 3 characters').optional().describe('Exact ICAO code (3 chars, e.g., "UAE")'),
227+
iata: z.string().regex(/^[A-Za-z0-9]{2}$/, 'IATA code must be 2 letters or digits (e.g., "EK", "U2")').optional().describe('Exact IATA code (2 alphanumeric chars)'),
228+
icao: z.string().regex(/^[A-Za-z]{3}$/, 'ICAO code must be 3 letters (e.g., "UAE", "BAW")').optional().describe('Exact ICAO code (3 letters)'),
229229
prefix: z.string().regex(/^\d{3}$/, 'AWB prefix must be exactly 3 digits').optional().describe('AWB prefix (3 digits, e.g., "176")'),
230230
country: z.string().min(2, 'Country must be at least 2 characters').optional().describe('Filter by country name (min 2 chars)'),
231231
}).strict(),
@@ -257,7 +257,7 @@ Use this tool when you need to:
257257
Provide a container type for specs. Add item dimensions (l, w, h in cm) to calculate loading.`,
258258

259259
schema: z.object({
260-
type: z.string().optional()
260+
type: z.string().min(2, 'Container type must be at least 2 characters').optional()
261261
.describe('Container slug (e.g., "20ft-standard", "40ft-high-cube"). Omit to list all types.'),
262262
item_length_cm: z.number().positive().optional().describe('Item length in cm (for loading calculation)'),
263263
item_width_cm: z.number().positive().optional().describe('Item width in cm'),
@@ -293,9 +293,9 @@ Use this tool when you need to:
293293
Provide a search term for description-based search, or an exact HS code for detailed lookup.`,
294294

295295
schema: z.object({
296-
query: z.string().optional().describe('Search by product description (min 2 chars)'),
297-
code: z.string().optional().describe('Exact HS code lookup (2-6 digits)'),
298-
section: z.string().optional().describe('Browse by section (Roman numeral, e.g., "II")'),
296+
query: z.string().min(2, 'Search term must be at least 2 characters').optional().describe('Search by product description (min 2 chars)'),
297+
code: z.string().regex(/^\d{2,6}$/, 'HS code must be 2–6 digits').optional().describe('Exact HS code lookup (2-6 digits)'),
298+
section: z.string().regex(/^[ivxIVX]{1,5}$/, 'Section must be a Roman numeral I–XXI').optional().describe('Browse by section (Roman numeral I–XXI, e.g., "II")'),
299299
}).strict(),
300300

301301
annotations: readOnlyAnnotations('HS Code Lookup'),
@@ -321,7 +321,7 @@ Use this tool when you need to:
321321
- Check which Incoterms apply to sea freight vs any mode`,
322322

323323
schema: z.object({
324-
code: z.string().optional().describe('Incoterm code (e.g., "FOB", "CIF", "EXW")'),
324+
code: z.string().regex(/^[A-Za-z]{3}$/, 'Incoterm code must be 3 letters (e.g., "FOB", "CIF", "EXW")').optional().describe('Incoterm code (3 letters, e.g., "FOB", "CIF", "EXW")'),
325325
category: z.enum(['any_mode', 'sea_only']).optional().describe('Filter by transport mode category'),
326326
}).strict(),
327327

@@ -393,8 +393,8 @@ Note: Short tons (US) = 2,000 lbs. Long tons (UK) = 2,240 lbs. Metric tonnes = 2
393393

394394
schema: z.object({
395395
value: z.number().describe('The value to convert'),
396-
from: z.string().describe('Source unit code (e.g., "kg", "cbm", "cm")'),
397-
to: z.string().describe('Target unit code (e.g., "lbs", "cuft", "inches")'),
396+
from: z.enum(['kg','lbs','oz','tonnes','short_tons','long_tons','cbm','cuft','cuin','litres','gal_us','gal_uk','cm','inches','m','feet','mm']).describe('Source unit — weight (kg, lbs, oz, tonnes, short_tons, long_tons), volume (cbm, cuft, cuin, litres, gal_us, gal_uk) or length (cm, inches, m, feet, mm)'),
397+
to: z.enum(['kg','lbs','oz','tonnes','short_tons','long_tons','cbm','cuft','cuin','litres','gal_us','gal_uk','cm','inches','m','feet','mm','chargeable_kg','freight_tonnes']).describe('Target unit — any source unit, plus the freight targets chargeable_kg and freight_tonnes (only valid from cbm)'),
398398
}).strict(),
399399

400400
annotations: readOnlyAnnotations('Unit Converter'),
@@ -476,9 +476,9 @@ Use this tool when you need to:
476476

477477
schema: z.object({
478478
query: z.string().min(2, 'Query must be at least 2 characters').optional().describe('Search by location name (e.g., "rotterdam", "heathrow")'),
479-
code: z.string().length(5, 'UN/LOCODE must be exactly 5 characters (2-letter country + 3-char location)').regex(/^[A-Z0-9]{5}$/i, 'UN/LOCODE must be alphanumeric').optional().describe('Exact UN/LOCODE lookup (e.g., "GBLHR", "NLRTM")'),
480-
country: z.string().length(2, 'Country must be a 2-letter ISO code').regex(/^[A-Z]{2}$/i, 'Country must be a 2-letter ISO code (e.g., "GB", "NL")').optional().describe('Filter by country code (e.g., "GB", "NL")'),
481-
function: z.string().optional().describe('Filter by function: port, airport, rail, road, icd, border'),
479+
code: z.string().regex(/^[A-Za-z0-9]{5}$/, 'UN/LOCODE must be 5 characters: 2-letter country + 3-char location (e.g. "GBLHR")').optional().describe('Exact UN/LOCODE lookup (e.g., "GBLHR", "NLRTM")'),
480+
country: z.string().regex(/^[A-Za-z]{2}$/, 'Country must be a 2-letter ISO code (e.g. "GB", "NL")').optional().describe('Filter by country code (e.g., "GB", "NL")'),
481+
function_type: z.enum(['port', 'airport', 'rail', 'road', 'icd', 'border']).optional().describe('Filter by location function'),
482482
limit: z.number().int().min(1).max(100).optional().describe('Max results (default: 20, max: 100)'),
483483
}).strict(),
484484

@@ -487,7 +487,7 @@ Use this tool when you need to:
487487
handler: async (args) =>
488488
apiGet('unlocode', {
489489
q: args.query, code: args.code, country: args.country,
490-
function: args.function, limit: args.limit,
490+
function: args.function_type, limit: args.limit,
491491
}),
492492
};
493493

@@ -509,11 +509,11 @@ Use this tool when you need to:
509509

510510
schema: z.object({
511511
commodity_code: z.string().regex(/^\d{6,10}$/, 'Commodity code must be 6–10 digits').describe('HS/tariff code (6–10 digits, e.g., "847989")'),
512-
origin_country: z.string().length(2, 'Origin country must be a 2-letter ISO code').regex(/^[A-Z]{2}$/i, 'Origin country must be a 2-letter ISO code (e.g., "CN", "DE")').describe('ISO 2-letter origin country code (e.g., "CN", "DE")'),
512+
origin_country: z.string().regex(/^[A-Za-z]{2}$/, 'Origin country must be a 2-letter ISO code (e.g., "CN", "DE")').describe('ISO 2-letter origin country code (e.g., "CN", "DE")'),
513513
customs_value: z.number().positive().describe('Goods value in GBP'),
514514
freight_cost: z.number().optional().describe('Freight cost in GBP (added to CIF value)'),
515515
insurance_cost: z.number().optional().describe('Insurance cost in GBP (added to CIF value)'),
516-
incoterm: z.string().optional().describe('Incoterm (e.g., "FOB", "CIF", "EXW")'),
516+
incoterm: z.enum(['EXW','FCA','FAS','FOB','CFR','CIF','CPT','CIP','DAP','DPU','DDP']).optional().describe('Incoterm basis'),
517517
}).strict(),
518518

519519
annotations: readOnlyAnnotations('UK Duty & VAT Calculator'),
@@ -604,7 +604,7 @@ Use this tool when you need to:
604604
- Find aircraft-compatible ULDs`,
605605

606606
schema: z.object({
607-
type: z.string().optional().describe('ULD code (e.g., "AKE", "PMC"). Omit to list all.'),
607+
type: z.string().min(2, 'ULD type must be at least 2 characters').optional().describe('ULD code (e.g., "AKE", "PMC") or slug (e.g., "ake-ld3"). Omit to list all.'),
608608
category: z.enum(['container', 'pallet', 'special']).optional().describe('Filter by category'),
609609
deck: z.enum(['lower', 'main']).optional().describe('Filter by deck position'),
610610
}).strict(),
@@ -632,7 +632,7 @@ Use this tool when you need to:
632632
- Check payload limits for heavy shipments`,
633633

634634
schema: z.object({
635-
slug: z.string().optional().describe('Vehicle slug (e.g., "standard-curtainsider"). Omit to list all.'),
635+
slug: z.string().min(2, 'Vehicle slug must be at least 2 characters').optional().describe('Vehicle slug (e.g., "standard-curtainsider"). Omit to list all.'),
636636
category: z.enum(['articulated', 'rigid', 'van']).optional().describe('Filter by category'),
637637
region: z.enum(['EU', 'US']).optional().describe('Filter by region'),
638638
}).strict(),

0 commit comments

Comments
 (0)