examples/MCP-tools Public
Accept unit names and plurals in convert_units
Models often pass what the user typed ("pounds", "°F", "kilometres") instead of the symbol, and each miss cost a retry. Every unit now has a list of names, matched case-insensitively with spaces collapsed. The result still reports the canonical symbol.
Changed files
2 files changed48 additions36 deletionsagainst 7ee1343
Changedsrc/tools/convert-units.ts+39−36
| @@ -8,52 +8,55 @@ | ||
| // Each unit converts to and from its quantity's base unit: metres, kilograms, litres, kelvin. | ||
| type Unit = { | ||
| symbol: string; | ||
| + // Other ways people and models write the unit, matched case-insensitively. | ||
| + names: string[]; | ||
| quantity: Quantity; | ||
| toBase: (v: number) => number; | ||
| fromBase: (v: number) => number; | ||
| }; | ||
| -function linear(quantity: Quantity, symbol: string, factor: number): Unit { | ||
| - return { symbol, quantity, toBase: (v) => v * factor, fromBase: (v) => v / factor }; | ||
| +function linear(quantity: Quantity, symbol: string, factor: number, names: string[]): Unit { | ||
| + return { symbol, names, quantity, toBase: (v) => v * factor, fromBase: (v) => v / factor }; | ||
| } | ||
| // Imperial volumes are US customary; the description says so. | ||
| const UNITS: Unit[] = [ | ||
| - linear("length", "mm", 0.001), | ||
| - linear("length", "cm", 0.01), | ||
| - linear("length", "m", 1), | ||
| - linear("length", "km", 1000), | ||
| - linear("length", "in", 0.0254), | ||
| - linear("length", "ft", 0.3048), | ||
| - linear("length", "yd", 0.9144), | ||
| - linear("length", "mi", 1609.344), | ||
| - linear("length", "nmi", 1852), | ||
| - linear("mass", "mg", 1e-6), | ||
| - linear("mass", "g", 0.001), | ||
| - linear("mass", "kg", 1), | ||
| - linear("mass", "t", 1000), | ||
| - linear("mass", "oz", 0.028349523125), | ||
| - linear("mass", "lb", 0.45359237), | ||
| - linear("mass", "st", 6.35029318), | ||
| - linear("volume", "ml", 0.001), | ||
| - linear("volume", "l", 1), | ||
| - linear("volume", "m3", 1000), | ||
| - linear("volume", "tsp", 0.00492892159375), | ||
| - linear("volume", "tbsp", 0.01478676478125), | ||
| - linear("volume", "fl-oz", 0.0295735295625), | ||
| - linear("volume", "cup", 0.2365882365), | ||
| - linear("volume", "pt", 0.473176473), | ||
| - linear("volume", "qt", 0.946352946), | ||
| - linear("volume", "gal", 3.785411784), | ||
| - { symbol: "C", quantity: "temperature", toBase: (v) => v + 273.15, fromBase: (v) => v - 273.15 }, | ||
| - { symbol: "F", quantity: "temperature", toBase: (v) => ((v - 32) * 5) / 9 + 273.15, fromBase: (v) => ((v - 273.15) * 9) / 5 + 32 }, | ||
| - { symbol: "K", quantity: "temperature", toBase: (v) => v, fromBase: (v) => v }, | ||
| + linear("length", "mm", 0.001, ["millimeter", "millimeters", "millimetre", "millimetres"]), | ||
| + linear("length", "cm", 0.01, ["centimeter", "centimeters", "centimetre", "centimetres"]), | ||
| + linear("length", "m", 1, ["meter", "meters", "metre", "metres"]), | ||
| + linear("length", "km", 1000, ["kilometer", "kilometers", "kilometre", "kilometres"]), | ||
| + linear("length", "in", 0.0254, ["inch", "inches", "\""]), | ||
| + linear("length", "ft", 0.3048, ["foot", "feet", "'"]), | ||
| + linear("length", "yd", 0.9144, ["yard", "yards"]), | ||
| + linear("length", "mi", 1609.344, ["mile", "miles"]), | ||
| + linear("length", "nmi", 1852, ["nautical mile", "nautical miles"]), | ||
| + linear("mass", "mg", 1e-6, ["milligram", "milligrams"]), | ||
| + linear("mass", "g", 0.001, ["gram", "grams"]), | ||
| + linear("mass", "kg", 1, ["kilogram", "kilograms", "kilo", "kilos"]), | ||
| + linear("mass", "t", 1000, ["tonne", "tonnes", "metric ton", "metric tons"]), | ||
| + linear("mass", "oz", 0.028349523125, ["ounce", "ounces"]), | ||
| + linear("mass", "lb", 0.45359237, ["lbs", "pound", "pounds"]), | ||
| + linear("mass", "st", 6.35029318, ["stone", "stones"]), | ||
| + linear("volume", "ml", 0.001, ["milliliter", "milliliters", "millilitre", "millilitres"]), | ||
| + linear("volume", "l", 1, ["liter", "liters", "litre", "litres"]), | ||
| + linear("volume", "m3", 1000, ["m³", "cubic meter", "cubic meters", "cubic metre", "cubic metres"]), | ||
| + linear("volume", "tsp", 0.00492892159375, ["teaspoon", "teaspoons"]), | ||
| + linear("volume", "tbsp", 0.01478676478125, ["tablespoon", "tablespoons"]), | ||
| + linear("volume", "fl-oz", 0.0295735295625, ["fl oz", "floz", "fluid ounce", "fluid ounces"]), | ||
| + linear("volume", "cup", 0.2365882365, ["cups"]), | ||
| + linear("volume", "pt", 0.473176473, ["pint", "pints"]), | ||
| + linear("volume", "qt", 0.946352946, ["quart", "quarts"]), | ||
| + linear("volume", "gal", 3.785411784, ["gallon", "gallons"]), | ||
| + { symbol: "C", names: ["°C", "celsius", "centigrade"], quantity: "temperature", toBase: (v) => v + 273.15, fromBase: (v) => v - 273.15 }, | ||
| + { symbol: "F", names: ["°F", "fahrenheit"], quantity: "temperature", toBase: (v) => ((v - 32) * 5) / 9 + 273.15, fromBase: (v) => ((v - 273.15) * 9) / 5 + 32 }, | ||
| + { symbol: "K", names: ["kelvin"], quantity: "temperature", toBase: (v) => v, fromBase: (v) => v }, | ||
| ]; | ||
| -const BY_KEY = new Map(UNITS.map((u) => [u.symbol.toLowerCase(), u])); | ||
| +const key = (text: string) => text.trim().toLowerCase().replace(/\s+/g, " "); | ||
| +const BY_KEY = new Map(UNITS.flatMap((u) => [u.symbol, ...u.names].map((n) => [key(n), u] as const))); | ||
| export function findUnit(text: string): Unit | undefined { | ||
| - return BY_KEY.get(text.trim().toLowerCase()); | ||
| + return BY_KEY.get(key(text)); | ||
| } | ||
| function unitOrThrow(text: string): Unit { | ||
| @@ -78,13 +81,13 @@ | ||
| description: | ||
| "Convert a value between units of length, mass, volume or temperature. " + | ||
| "Use it instead of doing the arithmetic yourself whenever the user needs an exact figure. " + | ||
| - "Units are short symbols: length mm, cm, m, km, in, ft, yd, mi, nmi; mass mg, g, kg, t, oz, lb, st; " + | ||
| + "Give units as symbols or names (km or kilometers, F or fahrenheit). Symbols: length mm, cm, m, km, in, ft, yd, mi, nmi; mass mg, g, kg, t, oz, lb, st; " + | ||
| "volume ml, l, m3, tsp, tbsp, fl-oz, cup, pt, qt, gal (US customary); temperature C, F, K. " + | ||
| "Both units must measure the same quantity.", | ||
| input: z.object({ | ||
| value: z.number().describe("The number to convert, in the `from` unit."), | ||
| - from: z.string().min(1).describe("The unit the value is in, for example km or F."), | ||
| - to: z.string().min(1).describe("The unit to convert to, for example mi or C."), | ||
| + from: z.string().min(1).describe("The unit the value is in, for example km, pounds or °F."), | ||
| + to: z.string().min(1).describe("The unit to convert to, for example mi, kg or celsius."), | ||
| }), | ||
| output: z.object({ | ||
| quantity: z.enum(QUANTITIES), | ||
Changedtest/convert-units.test.ts+9−0
| @@ -25,6 +25,15 @@ | ||
| assert.deepEqual(convert(2.5, "KM", " m "), { quantity: "length", value: 2.5, from: "km", result: 2500, to: "m" }); | ||
| }); | ||
| +test("accepts unit names, plurals and degree signs", () => { | ||
| + assert.equal(convert(3, "Feet", "inches").result, 36); | ||
| + assert.equal(convert(2, "kilometres", "meters").result, 2000); | ||
| + assert.equal(convert(1, "pounds", "Kilos").result, 0.45359237); | ||
| + assert.equal(convert(1, "fluid ounces", "tablespoons").result, 2); | ||
| + assert.equal(convert(212, "°F", "celsius").result, 100); | ||
| + assert.deepEqual(convert(1, "gallon", "liters"), { quantity: "volume", value: 1, from: "gal", result: 3.785411784, to: "l" }); | ||
| +}); | ||
| + | ||
| test("hides floating point noise", () => { | ||
| assert.equal(convert(0.1, "m", "cm").result, 10); | ||
| assert.equal(convert(0.3, "l", "ml").result, 300); | ||
Checks
No checks reported for this commit.
Runs
No runs for this commit.