openrouteservice-mcp 1.0.3 → 1.1.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/dist/api.js +99 -0
- package/dist/server.js +3 -5
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -94,6 +94,105 @@ function orsDuration(seconds) {
|
|
|
94
94
|
return `${m} min`;
|
|
95
95
|
return `${Math.floor(m / 60)} h ${m % 60} min`;
|
|
96
96
|
}
|
|
97
|
+
function orsCoord(value, limit) {
|
|
98
|
+
const n = Number(value);
|
|
99
|
+
if (!Number.isFinite(n) || Math.abs(n) > limit)
|
|
100
|
+
throw new OrsError(`Coordinate ${value} is out of range (|value| must be at most ${limit}).`);
|
|
101
|
+
return n;
|
|
102
|
+
}
|
|
103
|
+
function orsManoeuvre(step) {
|
|
104
|
+
const m = step?.maneuver ?? {};
|
|
105
|
+
const kind = [m.type, m.modifier].filter(Boolean).join(" ") || "continue";
|
|
106
|
+
return { kind, instruction: String(step?.name ? `on ${step.name}` : "unspecified road"), road: String(step?.name ?? "") };
|
|
107
|
+
}
|
|
108
|
+
/** Turn-by-turn steps from OSRM: a different router and data pipeline from this server's own directions. */
|
|
109
|
+
export async function osrmRouteSteps(args) {
|
|
110
|
+
const fromLat = orsCoord(args?.from_lat, 90);
|
|
111
|
+
const fromLon = orsCoord(args?.from_lon, 180);
|
|
112
|
+
const toLat = orsCoord(args?.to_lat, 90);
|
|
113
|
+
const toLon = orsCoord(args?.to_lon, 180);
|
|
114
|
+
const alternatives = args?.alternatives === true ? "true" : "false";
|
|
115
|
+
const coords = `${fromLon},${fromLat};${toLon},${toLat}`;
|
|
116
|
+
const url = `${ORS_OSRM}/route/v1/driving/${coords}?steps=true&alternatives=${alternatives}&overview=false&annotations=false`;
|
|
117
|
+
const data = await orsFetch(url);
|
|
118
|
+
if (String(data?.code ?? "") !== "Ok" || !Array.isArray(data?.routes) || !data.routes.length)
|
|
119
|
+
throw new OrsError(`OSRM found no drivable route: ${String(data?.message ?? data?.code ?? "no answer")}.`);
|
|
120
|
+
const rows = [];
|
|
121
|
+
const routes = data.routes.slice(0, alternatives === "true" ? 3 : 1);
|
|
122
|
+
rows.push(`OSRM driving route over OpenStreetMap data — ${routes.length} route(s) for ${coords}`);
|
|
123
|
+
routes.forEach((route, index) => {
|
|
124
|
+
const steps = (route.legs ?? []).flatMap((leg) => leg.steps ?? []);
|
|
125
|
+
const roads = [...new Set(steps.map((s) => String(s.name ?? "").trim()).filter(Boolean))];
|
|
126
|
+
rows.push("");
|
|
127
|
+
rows.push(`Route ${index + 1}: ${orsMetres(Number(route.distance ?? 0))} | ${orsDuration(Number(route.duration ?? 0))} | ${steps.length} manoeuvre(s)`);
|
|
128
|
+
rows.push(` distinct named roads: ${roads.length}${roads.length ? ` — ${roads.slice(0, 8).join(", ")}${roads.length > 8 ? ", …" : ""}` : " — none named"}`);
|
|
129
|
+
steps.forEach((step, i) => {
|
|
130
|
+
const m = step.maneuver ?? {};
|
|
131
|
+
const kind = [m.type, m.modifier].filter(Boolean).join(" ") || "continue";
|
|
132
|
+
rows.push(` ${String(i + 1).padStart(2)}. ${kind.padEnd(22)} ${String(step.name ?? "(unnamed)").slice(0, 40).padEnd(40)} ${orsMetres(Number(step.distance ?? 0))}`);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
rows.push("", "OSRM is a different engine on the same OpenStreetMap data as this server's directions tool, so distances and turn instructions will differ.");
|
|
136
|
+
return rows.join("\n");
|
|
137
|
+
}
|
|
138
|
+
/** The same journey costed by several Valhalla modes, side by side. */
|
|
139
|
+
export async function valhallaModeCompare(args) {
|
|
140
|
+
const fromLat = orsCoord(args?.from_lat, 90);
|
|
141
|
+
const fromLon = orsCoord(args?.from_lon, 180);
|
|
142
|
+
const toLat = orsCoord(args?.to_lat, 90);
|
|
143
|
+
const toLon = orsCoord(args?.to_lon, 180);
|
|
144
|
+
const offered = ["auto", "bicycle", "pedestrian", "multimodal", "truck", "taxi"];
|
|
145
|
+
const requested = String(args?.modes ?? "")
|
|
146
|
+
.split(",")
|
|
147
|
+
.map((m) => m.trim().toLowerCase())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
const modes = requested.length ? offered.filter((m) => requested.includes(m)) : ["auto", "bicycle", "pedestrian"];
|
|
150
|
+
if (!modes.length)
|
|
151
|
+
throw new OrsError(`None of ${requested.join(", ")} is a Valhalla costing here; use any of ${offered.join(", ")}.`);
|
|
152
|
+
const rows = [`Valhalla journey comparison for ${fromLat},${fromLon} to ${toLat},${toLon}`];
|
|
153
|
+
for (const mode of modes) {
|
|
154
|
+
let summary;
|
|
155
|
+
try {
|
|
156
|
+
const data = await orsFetch(`${ORS_VALHALLA}/route`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
locations: [{ lat: fromLat, lon: fromLon }, { lat: toLat, lon: toLon }],
|
|
161
|
+
costing: mode,
|
|
162
|
+
directions_options: { units: "kilometers" },
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const trip = data?.trip ?? {};
|
|
166
|
+
const legs = Array.isArray(trip.legs) ? trip.legs : [];
|
|
167
|
+
const maneuvers = legs.flatMap((leg) => Array.isArray(leg.maneuvers) ? leg.maneuvers : []);
|
|
168
|
+
summary = {
|
|
169
|
+
ok: true,
|
|
170
|
+
distance: Number(trip.summary?.length ?? maneuvers.reduce((s, m) => s + Number(m.length ?? 0), 0)) * 1000,
|
|
171
|
+
seconds: Number(trip.summary?.time ?? 0),
|
|
172
|
+
cost: trip.summary?.cost,
|
|
173
|
+
toll: Boolean(trip.summary?.has_toll),
|
|
174
|
+
ferry: Boolean(trip.summary?.has_ferry),
|
|
175
|
+
motorway: Boolean(trip.summary?.has_highway),
|
|
176
|
+
energy: data?.trip?.summary?.energy_consumption ?? null,
|
|
177
|
+
opening: maneuvers.slice(0, 3).map((m) => String(m.instruction ?? m.type ?? "manoeuvre").trim()),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
summary = { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
182
|
+
}
|
|
183
|
+
rows.push("");
|
|
184
|
+
if (!summary.ok) {
|
|
185
|
+
rows.push(`${mode.padEnd(11)} unavailable — ${summary.reason.slice(0, 140)}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
rows.push(`${mode.padEnd(11)} ${orsMetres(summary.distance)} | ${orsDuration(summary.seconds)} | toll ${summary.toll ? "yes" : "no"} | ferry ${summary.ferry ? "yes" : "no"} | motorway ${summary.motorway ? "yes" : "no"}`);
|
|
189
|
+
rows.push(` cost ${summary.cost === undefined || summary.cost === null ? "not returned" : `${Number(summary.cost).toFixed(2)} (Valhalla cost units, not a currency)`}${summary.energy !== null && summary.energy !== undefined ? ` | energy estimate ${summary.energy}` : ""}`);
|
|
190
|
+
if (summary.opening.length)
|
|
191
|
+
rows.push(` opens with: ${summary.opening.join(" → ")}`);
|
|
192
|
+
}
|
|
193
|
+
rows.push("", "Each mode is a separate Valhalla request against the public OSM instance; a mode that fails is reported rather than guessed, and cost/tolls only appear where the engine returns them.");
|
|
194
|
+
return rows.join("\n");
|
|
195
|
+
}
|
|
97
196
|
/** Geocode against OpenStreetMap itself, so you can see how the answer moves between geocoders. */
|
|
98
197
|
export async function nominatimLookup(args) {
|
|
99
198
|
const limit = orsBound(args?.limit, 1, ORS_LIST_CAP, 5);
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { geocode, reverse, directions, nominatimLookup } from "./api.js";
|
|
3
|
+
import { geocode, reverse, directions, nominatimLookup, osrmRouteSteps, valhallaModeCompare } from "./api.js";
|
|
4
4
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
5
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
6
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -89,9 +89,8 @@ export function createServer() {
|
|
|
89
89
|
}),
|
|
90
90
|
annotations: READ_ONLY,
|
|
91
91
|
}, async (args) => {
|
|
92
|
-
// TODO: implement steps locally (paywall removed)
|
|
93
92
|
try {
|
|
94
|
-
return
|
|
93
|
+
return text(await osrmRouteSteps(args));
|
|
95
94
|
}
|
|
96
95
|
catch (e) {
|
|
97
96
|
return textError(String(e));
|
|
@@ -109,9 +108,8 @@ export function createServer() {
|
|
|
109
108
|
}),
|
|
110
109
|
annotations: READ_ONLY,
|
|
111
110
|
}, async (args) => {
|
|
112
|
-
// TODO: implement compare locally (paywall removed)
|
|
113
111
|
try {
|
|
114
|
-
return
|
|
112
|
+
return text(await valhallaModeCompare(args));
|
|
115
113
|
}
|
|
116
114
|
catch (e) {
|
|
117
115
|
return textError(String(e));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openrouteservice-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Query OpenRouteService (free key; set OPENROUTESERVICE_API_KEY): geocode, reverse geocode, driving directions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"mcpName": "io.github.mrfentmen/openrouteservice-mcp",
|