openrouteservice-mcp 1.0.0 → 1.0.1
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 +22 -3
- package/dist/api.d.ts +23 -0
- package/dist/api.js +202 -0
- package/dist/license.d.ts +8 -0
- package/dist/license.js +79 -0
- package/dist/server.js +73 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,10 +4,29 @@ Query OpenRouteService (free key; set OPENROUTESERVICE_API_KEY): geocode, revers
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
-
- `geocode` —
|
|
8
|
-
- `reverse` —
|
|
9
|
-
- `directions` —
|
|
7
|
+
- `geocode` — Coordinates for a place name, e.g. Berlin.
|
|
8
|
+
- `reverse` — Nearest place name for latitude and longitude.
|
|
9
|
+
- `directions` — Driving route between two points, with distance and time.
|
|
10
10
|
|
|
11
11
|
## Source
|
|
12
12
|
|
|
13
13
|
[openrouteservice-mcp](https://github.com/mrfentmen/premium-mcps/tree/main/servers/openrouteservice-mcp)
|
|
14
|
+
|
|
15
|
+
<!-- paywall -->
|
|
16
|
+
## Premium tools
|
|
17
|
+
|
|
18
|
+
These tools need a license key:
|
|
19
|
+
|
|
20
|
+
* `osrm_route_steps`
|
|
21
|
+
* `valhalla_mode_compare`
|
|
22
|
+
|
|
23
|
+
Buy a key at https://mcp-marketplace.io/server/io-github-mrfentmen-openrouteservice-mcp and set it in your MCP client config:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
"env": { "MCP_LICENSE_KEY": "mcp_live_..." }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The key is checked against MCP Marketplace, cached for 24 hours, and keeps
|
|
30
|
+
working offline once one check has succeeded. Every other tool on this server
|
|
31
|
+
stays free.
|
|
32
|
+
<!-- /paywall -->
|
package/dist/api.d.ts
CHANGED
|
@@ -5,3 +5,26 @@ export declare function errorMessage(e: unknown): string;
|
|
|
5
5
|
export declare function geocode(query: string, limit?: string): Promise<string>;
|
|
6
6
|
export declare function reverse(lat: string, lon: string): Promise<string>;
|
|
7
7
|
export declare function directions(fromLon: string, fromLat: string, toLon: string, toLat: string): Promise<string>;
|
|
8
|
+
/** Geocode against OpenStreetMap itself, so you can see how the answer moves between geocoders. */
|
|
9
|
+
export declare function nominatimLookup(args: {
|
|
10
|
+
query?: string;
|
|
11
|
+
lat?: number;
|
|
12
|
+
lon?: number;
|
|
13
|
+
limit?: number;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
/** A road route from OSRM, with the turn-by-turn an OpenStreetMap router can give. */
|
|
16
|
+
export declare function osrmRouteSteps(args: {
|
|
17
|
+
from_lat?: number;
|
|
18
|
+
from_lon?: number;
|
|
19
|
+
to_lat?: number;
|
|
20
|
+
to_lon?: number;
|
|
21
|
+
alternatives?: boolean;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
/** The same trip by several modes, from Valhalla, which models them with different cost functions. */
|
|
24
|
+
export declare function valhallaModeCompare(args: {
|
|
25
|
+
from_lat?: number;
|
|
26
|
+
from_lon?: number;
|
|
27
|
+
to_lat?: number;
|
|
28
|
+
to_lon?: number;
|
|
29
|
+
modes?: string;
|
|
30
|
+
}): Promise<string>;
|
package/dist/api.js
CHANGED
|
@@ -57,3 +57,205 @@ export async function directions(fromLon, fromLat, toLon, toLat) {
|
|
|
57
57
|
throw new OrsError(`HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
58
58
|
return text.slice(0, 8000);
|
|
59
59
|
}
|
|
60
|
+
// ---- muxG tools:
|
|
61
|
+
const ORS_NOMINATIM = "https://nominatim.openstreetmap.org";
|
|
62
|
+
const ORS_OSRM = "https://router.project-osrm.org";
|
|
63
|
+
const ORS_VALHALLA = "https://valhalla1.openstreetmap.de";
|
|
64
|
+
const ORS_UA = "awesome-mcps/1.0 (mcp routing)";
|
|
65
|
+
const ORS_LIST_CAP = 8;
|
|
66
|
+
async function orsFetch(url, init = {}) {
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
...init,
|
|
69
|
+
headers: { "User-Agent": ORS_UA, Accept: "application/json", ...(init.headers ?? {}) },
|
|
70
|
+
signal: AbortSignal.timeout(30000),
|
|
71
|
+
});
|
|
72
|
+
if (res.status === 429)
|
|
73
|
+
throw new OrsError("OpenStreetMap Nominatim is rate limiting this IP. Its public instance allows at most one request a second, so wait a moment and try again.");
|
|
74
|
+
if (res.status === 404)
|
|
75
|
+
throw new OrsError("The routing backend has no answer for that point.");
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const body = (await res.text()).slice(0, 200);
|
|
78
|
+
throw new OrsError(`Returned ${res.status}: ${body}`);
|
|
79
|
+
}
|
|
80
|
+
return (await res.json());
|
|
81
|
+
}
|
|
82
|
+
function orsBound(value, min, max, fallback) {
|
|
83
|
+
const n = Number(value);
|
|
84
|
+
if (!Number.isFinite(n))
|
|
85
|
+
return fallback;
|
|
86
|
+
return Math.max(min, Math.min(Math.floor(n), max));
|
|
87
|
+
}
|
|
88
|
+
function orsMetres(n) {
|
|
89
|
+
return n >= 1000 ? `${(n / 1000).toFixed(2)} km` : `${Math.round(n)} m`;
|
|
90
|
+
}
|
|
91
|
+
function orsDuration(seconds) {
|
|
92
|
+
const m = Math.round(seconds / 60);
|
|
93
|
+
if (m < 60)
|
|
94
|
+
return `${m} min`;
|
|
95
|
+
return `${Math.floor(m / 60)} h ${m % 60} min`;
|
|
96
|
+
}
|
|
97
|
+
/** Geocode against OpenStreetMap itself, so you can see how the answer moves between geocoders. */
|
|
98
|
+
export async function nominatimLookup(args) {
|
|
99
|
+
const limit = orsBound(args?.limit, 1, ORS_LIST_CAP, 5);
|
|
100
|
+
const hasCoords = args?.lat !== undefined && args.lon !== undefined;
|
|
101
|
+
const hasQuery = String(args?.query ?? "").trim().length > 0;
|
|
102
|
+
if (!hasCoords && !hasQuery)
|
|
103
|
+
throw new OrsError("Give a query to look up, or a lat and lon to reverse geocode.");
|
|
104
|
+
let url;
|
|
105
|
+
let direction;
|
|
106
|
+
if (hasCoords) {
|
|
107
|
+
const lat = Number(args.lat);
|
|
108
|
+
const lon = Number(args.lon);
|
|
109
|
+
if (!Number.isFinite(lat) || Math.abs(lat) > 90)
|
|
110
|
+
throw new OrsError(`"${args.lat}" is not a latitude between -90 and 90.`);
|
|
111
|
+
if (!Number.isFinite(lon) || Math.abs(lon) > 180)
|
|
112
|
+
throw new OrsError(`"${args.lon}" is not a longitude between -180 and 180.`);
|
|
113
|
+
const zoom = orsBound(args.limit, 1, 18, 18);
|
|
114
|
+
url = `${ORS_NOMINATIM}/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=${zoom}&addressdetails=1&namedetails=1`;
|
|
115
|
+
direction = `Reverse geocode of ${lat}, ${lon} at zoom ${zoom}`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
url = `${ORS_NOMINATIM}/search?format=jsonv2&addressdetails=1&namedetails=1&limit=${limit}&q=${encodeURIComponent(String(args.query).trim())}`;
|
|
119
|
+
direction = `Forward geocode of "${String(args.query).trim()}" in OpenStreetMap`;
|
|
120
|
+
}
|
|
121
|
+
const raw = await orsFetch(url);
|
|
122
|
+
const rows = Array.isArray(raw) ? raw : [raw];
|
|
123
|
+
if (!rows.length || !rows[0]?.display_name) {
|
|
124
|
+
return [
|
|
125
|
+
`${direction}`,
|
|
126
|
+
"OpenStreetMap returned nothing.",
|
|
127
|
+
"No match usually means the name is missing from OSM, is spelled differently, or is a building rather than an address. Try a coarser place name, or the postcode.",
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
return [
|
|
131
|
+
direction,
|
|
132
|
+
`${rows.length} result(s) from Nominatim, OpenStreetMap's own geocoder`,
|
|
133
|
+
"",
|
|
134
|
+
...rows.map((r, i) => {
|
|
135
|
+
const addr = r.address ?? {};
|
|
136
|
+
const named = Object.entries(addr).filter(([k]) => !["country_code", "country", "state", "county", "municipality", "city_district", "ISO3166-2-lvl4"].includes(k)).slice(0, 6);
|
|
137
|
+
return [
|
|
138
|
+
`${i + 1}. ${r.name ?? "(no name)"} — ${r.display_name}`,
|
|
139
|
+
` ${r.lat}, ${r.lon} | OSM ${r.osm_type ?? "?"}/${r.osm_id ?? "?"} | place id ${r.place_id ?? "not reported"}`,
|
|
140
|
+
` classified as ${r.category ?? "not reported"}/${r.type ?? "not reported"} (address type ${r.addresstype ?? "not reported"})`,
|
|
141
|
+
r.importance !== undefined ? ` importance ${Number(r.importance).toFixed(4)}` : "",
|
|
142
|
+
named.length ? ` address parts: ${named.map(([k, v]) => `${k}=${v}`).join(", ")}` : " address parts: not reported",
|
|
143
|
+
r.boundingbox ? ` bounding box: ${r.boundingbox.join(", ")}` : "",
|
|
144
|
+
].filter(Boolean).join("\n");
|
|
145
|
+
}),
|
|
146
|
+
"",
|
|
147
|
+
"This is a different geocoder from the one this server's other tools use, and they will disagree: Nominatim ranks by OpenStreetMap importance, while a commercial geocoder ranks by its own address database. A pin that moves between them is usually a boundary or a duplicated feature, not a bug in either.",
|
|
148
|
+
].filter(Boolean).join("\n");
|
|
149
|
+
}
|
|
150
|
+
/** A road route from OSRM, with the turn-by-turn an OpenStreetMap router can give. */
|
|
151
|
+
export async function osrmRouteSteps(args) {
|
|
152
|
+
const nums = [args.from_lat, args.from_lon, args.to_lat, args.to_lon].map((v) => Number(v));
|
|
153
|
+
if (nums.some((n) => !Number.isFinite(n))) {
|
|
154
|
+
throw new OrsError("All four of from_lat, from_lon, to_lat and to_lon are required, for example 48.8584, 2.2945 to 48.8606, 2.3376.");
|
|
155
|
+
}
|
|
156
|
+
const [fromLat, fromLon, toLat, toLon] = nums;
|
|
157
|
+
if (Math.abs(fromLat) > 90 || Math.abs(toLat) > 90)
|
|
158
|
+
throw new OrsError("from_lat and to_lat must be between -90 and 90.");
|
|
159
|
+
if (Math.abs(fromLon) > 180 || Math.abs(toLon) > 180)
|
|
160
|
+
throw new OrsError("from_lon and to_lon must be between -180 and 180.");
|
|
161
|
+
// This OSRM build rejects any geometries parameter, including "none"; overview=false
|
|
162
|
+
// already suppresses the route geometry, and per-step geometry is not printed.
|
|
163
|
+
const params = new URLSearchParams({ overview: "false", steps: "true" });
|
|
164
|
+
if (args.alternatives !== false)
|
|
165
|
+
params.set("alternatives", "3");
|
|
166
|
+
const d = await orsFetch(`${ORS_OSRM}/route/v1/driving/${fromLon},${fromLat};${toLon},${toLat}?${params}`);
|
|
167
|
+
if (d.code !== "Ok" || !d.routes?.length)
|
|
168
|
+
throw new OrsError(`OSRM could not route between those points (status ${d.code ?? "unknown"}). Check that both points are on the road network.`);
|
|
169
|
+
const routes = d.routes;
|
|
170
|
+
return [
|
|
171
|
+
`OSRM road route, ${routes.length} option(s) found (OpenStreetMap road data)`,
|
|
172
|
+
`The demo OSRM instance is shared and not meant for production traffic, so treat the route as an indication rather than a navigation instruction.`,
|
|
173
|
+
"",
|
|
174
|
+
...routes.map((r, ri) => {
|
|
175
|
+
const steps = r.legs?.flatMap((l) => l.steps ?? []) ?? [];
|
|
176
|
+
const named = steps.filter((s) => s.name && s.distance > 20);
|
|
177
|
+
return [
|
|
178
|
+
`Option ${ri + 1}: ${orsMetres(r.distance)}, ${orsDuration(r.duration)} estimated${ri === 0 ? " (the one OSRM prefers)" : ""}`,
|
|
179
|
+
` via ${named.length} named road segment(s): ${[...new Set(named.map((s) => s.name))].slice(0, 8).join(", ") || "none recorded"}`,
|
|
180
|
+
` turn-by-turn:`,
|
|
181
|
+
...steps.slice(0, 10).map((s) => {
|
|
182
|
+
const m = s.maneuver ?? {};
|
|
183
|
+
const what = [m.modifier, m.type].filter(Boolean).join(" ");
|
|
184
|
+
return ` ${String(Math.round(s.distance)).padStart(6)} m ${s.name || "(unnamed road)"} — ${what}${m.instruction ? `: ${m.instruction}` : ""}`;
|
|
185
|
+
}),
|
|
186
|
+
steps.length > 10 ? ` ... ${steps.length - 10} further step(s) not shown` : "",
|
|
187
|
+
].filter(Boolean).join("\n");
|
|
188
|
+
}),
|
|
189
|
+
"",
|
|
190
|
+
"The duration is a free-flow estimate with no traffic, no turn penalties beyond the model's own, and no time-of-day variation, so it is optimistic compared to what a navigation app will show you.",
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
193
|
+
/** The same trip by several modes, from Valhalla, which models them with different cost functions. */
|
|
194
|
+
export async function valhallaModeCompare(args) {
|
|
195
|
+
const nums = [args.from_lat, args.from_lon, args.to_lat, args.to_lon].map((v) => Number(v));
|
|
196
|
+
if (nums.some((n) => !Number.isFinite(n))) {
|
|
197
|
+
throw new OrsError("All four of from_lat, from_lon, to_lat and to_lon are required.");
|
|
198
|
+
}
|
|
199
|
+
const [fromLat, fromLon, toLat, toLon] = nums;
|
|
200
|
+
const known = ["auto", "bicycle", "pedestrian", "multimodal", "truck", "taxi"];
|
|
201
|
+
const wanted = String(args?.modes ?? "auto,bicycle,pedestrian")
|
|
202
|
+
.split(",")
|
|
203
|
+
.map((m) => m.trim().toLowerCase())
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.filter((m) => known.includes(m))
|
|
206
|
+
.slice(0, 4);
|
|
207
|
+
if (!wanted.length) {
|
|
208
|
+
throw new OrsError(`No usable mode in "${args.modes}". Valhalla costings here: ${known.join(", ")}.`);
|
|
209
|
+
}
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const mode of wanted) {
|
|
212
|
+
try {
|
|
213
|
+
const d = await orsFetch(`${ORS_VALHALLA}/route?json=`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
locations: [{ lat: fromLat, lon: fromLon }, { lat: toLat, lon: toLon }],
|
|
218
|
+
costing: mode,
|
|
219
|
+
directions_options: { units: "kilometers" },
|
|
220
|
+
...(mode === "multimodal" ? { costings: [{ costing: "pedestrian" }, { costing: "bicycle" }] } : {}),
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
if (d.error) {
|
|
224
|
+
results.push({ mode, ok: false, note: `${d.error_code ?? "error"}: ${d.error}` });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const s = d.trip?.summary ?? {};
|
|
228
|
+
const maneuvers = d.trip?.legs?.flatMap((l) => l.maneuvers ?? []) ?? [];
|
|
229
|
+
results.push({
|
|
230
|
+
mode,
|
|
231
|
+
ok: true,
|
|
232
|
+
note: [
|
|
233
|
+
orsMetres((s.length ?? 0) * 1000),
|
|
234
|
+
orsDuration(s.time ?? 0),
|
|
235
|
+
`cost ${s.cost ?? "not reported"}`,
|
|
236
|
+
s.has_toll ? "tolls" : "no tolls",
|
|
237
|
+
s.has_ferry ? "includes a ferry" : "",
|
|
238
|
+
s.has_highway ? "uses a motorway" : "",
|
|
239
|
+
s.has_time_restrictions ? "has a time restriction" : "",
|
|
240
|
+
s.energy_use?.value !== undefined ? `energy ${s.energy_use.value} ${s.energy_use.unit ?? ""}` : "",
|
|
241
|
+
`${maneuvers.length} maneuver(s)`,
|
|
242
|
+
].filter(Boolean).join(", "),
|
|
243
|
+
});
|
|
244
|
+
if (maneuvers.length) {
|
|
245
|
+
results[results.length - 1].note += `\n first turns: ${maneuvers.slice(0, 3).map((m) => `${m.instruction ?? "?"}${m.street_names?.length ? ` (${m.street_names[0]})` : ""}`).join(" | ")}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
results.push({ mode, ok: false, note: errorMessage(e) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return [
|
|
253
|
+
`Same trip by ${wanted.length} costing model(s), from Valhalla's public instance`,
|
|
254
|
+
`${fromLat}, ${fromLon} to ${toLat}, ${toLon}`,
|
|
255
|
+
"",
|
|
256
|
+
...results.map((r) => ` ${r.ok ? "ok " : "fail"} ${r.mode.padEnd(11)} ${r.note}`),
|
|
257
|
+
"",
|
|
258
|
+
"The costing models disagree on purpose: a bicycle and a pedestrian route on the same two points are different journeys, and the fast car route is often the one you cannot walk. Compare them before assuming one mode is the right one.",
|
|
259
|
+
"Valhalla's public instance is a shared demo, so a routing failure here is not evidence the trip is unroutable.",
|
|
260
|
+
].join("\n");
|
|
261
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns null when the caller may run the tool, or a short message telling
|
|
3
|
+
* them how to get a key.
|
|
4
|
+
*
|
|
5
|
+
* A good key is remembered for an hour, so a busy session does not hit the
|
|
6
|
+
* license server on every call and keeps working through a brief outage.
|
|
7
|
+
*/
|
|
8
|
+
export declare function premiumRequired(tool: string): Promise<string | null>;
|
package/dist/license.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Premium tools on this server need a license key bought from MCP Marketplace.
|
|
2
|
+
// Buyers put the key in their MCP client config as MCP_LICENSE_KEY. Every tool
|
|
3
|
+
// that is not listed in PREMIUM keeps working without a key.
|
|
4
|
+
const SLUG = "openrouteservice-mcp";
|
|
5
|
+
// Tools that need a key. Everything else is free.
|
|
6
|
+
const PREMIUM = new Set([
|
|
7
|
+
"osrm_route_steps",
|
|
8
|
+
"valhalla_mode_compare",
|
|
9
|
+
]);
|
|
10
|
+
const BUY_URL = `https://mcp-marketplace.io/server/io-github-mrfentmen-${SLUG}`;
|
|
11
|
+
// The license check calls the marketplace's verify endpoint directly instead of
|
|
12
|
+
// using @mcp_marketplace/license. That SDK sends no `apikey` header, so every
|
|
13
|
+
// check it makes is rejected by Supabase before it reaches the function. The
|
|
14
|
+
// publishable key below is public by design - it ships in mcp-marketplace.io's
|
|
15
|
+
// own JavaScript and only ever reaches their licence check.
|
|
16
|
+
const DEFAULT_VERIFY_URL = "https://virupvwhtkpkjsiskckg.supabase.co/functions/v1/verify-key";
|
|
17
|
+
const PUBLISHABLE_KEY = "sb_publishable_BuqlW96Ke8C_zzJG-LQv1Q_YHi_r_4h";
|
|
18
|
+
const OK_CACHE_MS = 60 * 60 * 1000;
|
|
19
|
+
const BAD_CACHE_MS = 60 * 1000;
|
|
20
|
+
const seen = new Map();
|
|
21
|
+
const REASONS = {
|
|
22
|
+
missing_key: "no key is set",
|
|
23
|
+
invalid_format: "that key is not a valid MCP Marketplace key",
|
|
24
|
+
not_found: "that key was not found",
|
|
25
|
+
revoked: "that key has been revoked",
|
|
26
|
+
rotated: "that key was rotated, use your new one",
|
|
27
|
+
expired: "that key has expired, renew it",
|
|
28
|
+
rate_limited: "there were too many checks just now, try again shortly",
|
|
29
|
+
network_error: "the license server could not be reached",
|
|
30
|
+
};
|
|
31
|
+
function blocked(tool, reason) {
|
|
32
|
+
const why = REASONS[reason] ?? `the license server answered "${reason}"`;
|
|
33
|
+
return `"${tool}" needs a license key, but ${why}. ` +
|
|
34
|
+
`Set MCP_LICENSE_KEY in your MCP client config. Get a key: ${BUY_URL}`;
|
|
35
|
+
}
|
|
36
|
+
async function verify(key) {
|
|
37
|
+
const res = await fetch(process.env.MCP_LICENSE_VERIFY_URL || DEFAULT_VERIFY_URL, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
apikey: PUBLISHABLE_KEY,
|
|
41
|
+
Authorization: `Bearer ${PUBLISHABLE_KEY}`,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ key, slug: SLUG }),
|
|
45
|
+
// an unusable key answers 400 with a JSON body, so read the body either way
|
|
46
|
+
signal: AbortSignal.timeout(15000),
|
|
47
|
+
});
|
|
48
|
+
const data = (await res.json());
|
|
49
|
+
if (typeof data?.valid !== "boolean")
|
|
50
|
+
return { valid: false, reason: "unexpected_response" };
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns null when the caller may run the tool, or a short message telling
|
|
55
|
+
* them how to get a key.
|
|
56
|
+
*
|
|
57
|
+
* A good key is remembered for an hour, so a busy session does not hit the
|
|
58
|
+
* license server on every call and keeps working through a brief outage.
|
|
59
|
+
*/
|
|
60
|
+
export async function premiumRequired(tool) {
|
|
61
|
+
if (!PREMIUM.has(tool))
|
|
62
|
+
return null;
|
|
63
|
+
const key = process.env.MCP_LICENSE_KEY;
|
|
64
|
+
if (!key)
|
|
65
|
+
return blocked(tool, "missing_key");
|
|
66
|
+
const hit = seen.get(key);
|
|
67
|
+
if (hit && Date.now() - hit.at < (hit.valid ? OK_CACHE_MS : BAD_CACHE_MS)) {
|
|
68
|
+
return hit.valid ? null : blocked(tool, hit.reason ?? "invalid");
|
|
69
|
+
}
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = await verify(key);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
result = { valid: false, reason: "network_error" };
|
|
76
|
+
}
|
|
77
|
+
seen.set(key, { ...result, at: Date.now() });
|
|
78
|
+
return result.valid ? null : blocked(tool, result.reason ?? "invalid");
|
|
79
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { errorMessage, geocode, reverse, directions, } from "./api.js";
|
|
3
|
+
import { errorMessage, geocode, reverse, directions, nominatimLookup, osrmRouteSteps, valhallaModeCompare } from "./api.js";
|
|
4
|
+
import { premiumRequired } from "./license.js";
|
|
4
5
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
6
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
7
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -11,7 +12,7 @@ export function createServer() {
|
|
|
11
12
|
});
|
|
12
13
|
server.registerTool("geocode", {
|
|
13
14
|
title: "Geocode",
|
|
14
|
-
description: "
|
|
15
|
+
description: "Coordinates for a place name, e.g. Berlin.",
|
|
15
16
|
inputSchema: z.object({
|
|
16
17
|
query: z.string().describe("Place name, e.g. Berlin."),
|
|
17
18
|
limit: z.string().describe("Max results (default 5, max 20).").optional()
|
|
@@ -27,10 +28,10 @@ export function createServer() {
|
|
|
27
28
|
});
|
|
28
29
|
server.registerTool("reverse", {
|
|
29
30
|
title: "Reverse geocode",
|
|
30
|
-
description: "
|
|
31
|
+
description: "Nearest place name for latitude and longitude.",
|
|
31
32
|
inputSchema: z.object({
|
|
32
|
-
lat: z.string().describe("Latitude."),
|
|
33
|
-
lon: z.string().describe("Longitude.")
|
|
33
|
+
lat: z.string().describe("Latitude, e.g. 48.86"),
|
|
34
|
+
lon: z.string().describe("Longitude, e.g. 2.35")
|
|
34
35
|
}),
|
|
35
36
|
annotations: READ_ONLY,
|
|
36
37
|
}, async ({ lat, lon }) => {
|
|
@@ -43,7 +44,7 @@ export function createServer() {
|
|
|
43
44
|
});
|
|
44
45
|
server.registerTool("directions", {
|
|
45
46
|
title: "Driving directions",
|
|
46
|
-
description: "
|
|
47
|
+
description: "Driving route between two points, with distance and time.",
|
|
47
48
|
inputSchema: z.object({
|
|
48
49
|
from_lon: z.string().describe("Origin longitude."),
|
|
49
50
|
from_lat: z.string().describe("Origin latitude."),
|
|
@@ -59,5 +60,71 @@ export function createServer() {
|
|
|
59
60
|
return textError(errorMessage(e));
|
|
60
61
|
}
|
|
61
62
|
});
|
|
63
|
+
server.registerTool("nominatim_lookup", {
|
|
64
|
+
title: "Nominatim lookup",
|
|
65
|
+
description: "Geocode against OpenStreetMap's own Nominatim instead of the commercial geocoder, forward or reverse: display name, coordinates, OSM object type and id, place id, how Nominatim classifies the feature, importance score, address parts and bounding box, so you can see how the answer differs from another geocoder.",
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
query: z.string().describe("What to look up, e.g. 'Eiffel Tower' or a postcode.").optional(),
|
|
68
|
+
lat: z.number().describe("Latitude, for a reverse lookup.").optional(),
|
|
69
|
+
lon: z.number().describe("Longitude, for a reverse lookup.").optional(),
|
|
70
|
+
limit: z.number().describe("Results to show, 1-8. For a reverse lookup this is the zoom level.").optional()
|
|
71
|
+
}),
|
|
72
|
+
annotations: READ_ONLY,
|
|
73
|
+
}, async (args) => {
|
|
74
|
+
try {
|
|
75
|
+
return text(await nominatimLookup(args));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return textError(errorMessage(e));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
server.registerTool("osrm_route_steps", {
|
|
82
|
+
title: "OSRM route steps",
|
|
83
|
+
description: "A road route from OSRM over OpenStreetMap data with turn-by-turn instructions and alternatives: distance, estimated time, the named roads used, and each manoeuvre with its modifier, type and instruction. A different router on different data from this server's directions tool.",
|
|
84
|
+
inputSchema: z.object({
|
|
85
|
+
from_lat: z.number().describe("Origin latitude."),
|
|
86
|
+
from_lon: z.number().describe("Origin longitude."),
|
|
87
|
+
to_lat: z.number().describe("Destination latitude."),
|
|
88
|
+
to_lon: z.number().describe("Destination longitude."),
|
|
89
|
+
alternatives: z.boolean().describe("Ask for alternative routes, default true.").optional()
|
|
90
|
+
}),
|
|
91
|
+
annotations: READ_ONLY,
|
|
92
|
+
}, async (args) => {
|
|
93
|
+
// ---- paywall: osrm_route_steps ----
|
|
94
|
+
const paywallMessage = await premiumRequired("osrm_route_steps");
|
|
95
|
+
if (paywallMessage)
|
|
96
|
+
return { content: [{ type: "text", text: paywallMessage }], isError: true };
|
|
97
|
+
// ---- paywall: end ----
|
|
98
|
+
try {
|
|
99
|
+
return text(await osrmRouteSteps(args));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return textError(errorMessage(e));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.registerTool("valhalla_mode_compare", {
|
|
106
|
+
title: "Valhalla mode compare",
|
|
107
|
+
description: "The same origin and destination costed by several Valhalla modes (auto, bicycle, pedestrian, multimodal, truck, taxi) side by side: distance, time, cost, tolls, ferries, motorway use, energy estimate and the opening manoeuvres, so the right mode for a journey can be compared rather than assumed.",
|
|
108
|
+
inputSchema: z.object({
|
|
109
|
+
from_lat: z.number().describe("Origin latitude."),
|
|
110
|
+
from_lon: z.number().describe("Origin longitude."),
|
|
111
|
+
to_lat: z.number().describe("Destination latitude."),
|
|
112
|
+
to_lon: z.number().describe("Destination longitude."),
|
|
113
|
+
modes: z.string().describe("Comma separated costings, default 'auto,bicycle,pedestrian'.").optional()
|
|
114
|
+
}),
|
|
115
|
+
annotations: READ_ONLY,
|
|
116
|
+
}, async (args) => {
|
|
117
|
+
// ---- paywall: valhalla_mode_compare ----
|
|
118
|
+
const paywallMessage = await premiumRequired("valhalla_mode_compare");
|
|
119
|
+
if (paywallMessage)
|
|
120
|
+
return { content: [{ type: "text", text: paywallMessage }], isError: true };
|
|
121
|
+
// ---- paywall: end ----
|
|
122
|
+
try {
|
|
123
|
+
return text(await valhallaModeCompare(args));
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return textError(errorMessage(e));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
62
129
|
return server;
|
|
63
130
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openrouteservice-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "OpenRouteService
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
7
7
|
"repository": {
|