openrouteservice-mcp 1.0.2 → 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/README.md CHANGED
@@ -12,21 +12,9 @@ Query OpenRouteService (free key; set OPENROUTESERVICE_API_KEY): geocode, revers
12
12
 
13
13
  [openrouteservice-mcp](https://github.com/mrfentmen/premium-mcps/tree/main/servers/openrouteservice-mcp)
14
14
 
15
- <!-- paywall -->
16
- ## Premium tools
15
+ ## Licence — no paywall
17
16
 
18
- These tools need a licence key:
19
-
20
- * `osrm_route_steps`
21
- * `valhalla_mode_compare`
22
-
23
- The tool bodies run on our gateway, not inside this package, so the key is what buys access. Buy one at https://buy.polar.sh/polar_cl_mrO7rS5LZxpqM7oAFFEA10i9121J240U40IIj4JRZO4 and set it in your MCP client config:
24
-
25
- ```json
26
- "env": { "MCP_LICENSE_KEY": "polar_..." }
27
- ```
28
-
29
- The gateway checks the key against Polar and gives every key 1,000 free premium
30
- calls a month before per-call billing starts. Every other tool on this server
31
- stays free.
32
- <!-- /paywall -->
17
+ Every tool in this package is free: no licence key, no premium tier, no hosted
18
+ licence gateway. The paywall machinery this repo once carried (per-tool licence
19
+ checks and paid hosted endpoints) was removed repo-wide on 2026-10-17. Nothing
20
+ that this package installs or calls asks you to pay.
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,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { errorMessage, geocode, reverse, directions, nominatimLookup } from "./api.js";
4
- import { premiumTool } from "./gateway.js";
3
+ import { geocode, reverse, directions, nominatimLookup, osrmRouteSteps, valhallaModeCompare } from "./api.js";
5
4
  const text = (t) => ({ content: [{ type: "text", text: t }] });
6
5
  const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
7
6
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
@@ -23,7 +22,7 @@ export function createServer() {
23
22
  return text(await geocode(query, limit));
24
23
  }
25
24
  catch (e) {
26
- return textError(errorMessage(e));
25
+ return textError(String(e));
27
26
  }
28
27
  });
29
28
  server.registerTool("reverse", {
@@ -39,7 +38,7 @@ export function createServer() {
39
38
  return text(await reverse(lat, lon));
40
39
  }
41
40
  catch (e) {
42
- return textError(errorMessage(e));
41
+ return textError(String(e));
43
42
  }
44
43
  });
45
44
  server.registerTool("directions", {
@@ -57,7 +56,7 @@ export function createServer() {
57
56
  return text(await directions(from_lon, from_lat, to_lon, to_lat));
58
57
  }
59
58
  catch (e) {
60
- return textError(errorMessage(e));
59
+ return textError(String(e));
61
60
  }
62
61
  });
63
62
  server.registerTool("nominatim_lookup", {
@@ -75,7 +74,7 @@ export function createServer() {
75
74
  return text(await nominatimLookup(args));
76
75
  }
77
76
  catch (e) {
78
- return textError(errorMessage(e));
77
+ return textError(String(e));
79
78
  }
80
79
  });
81
80
  server.registerTool("osrm_route_steps", {
@@ -90,14 +89,11 @@ export function createServer() {
90
89
  }),
91
90
  annotations: READ_ONLY,
92
91
  }, async (args) => {
93
- // ---- paywall: osrm_route_steps ----
94
- // The body runs on the gateway, behind the licence gate.
95
- // ---- paywall: end ----
96
92
  try {
97
- return text(await premiumTool("mcp_openrouteservice_osrm_route_steps", args));
93
+ return text(await osrmRouteSteps(args));
98
94
  }
99
95
  catch (e) {
100
- return textError(errorMessage(e));
96
+ return textError(String(e));
101
97
  }
102
98
  });
103
99
  server.registerTool("valhalla_mode_compare", {
@@ -112,15 +108,45 @@ export function createServer() {
112
108
  }),
113
109
  annotations: READ_ONLY,
114
110
  }, async (args) => {
115
- // ---- paywall: valhalla_mode_compare ----
116
- // The body runs on the gateway, behind the licence gate.
117
- // ---- paywall: end ----
118
111
  try {
119
- return text(await premiumTool("mcp_openrouteservice_valhalla_mode_compare", args));
112
+ return text(await valhallaModeCompare(args));
120
113
  }
121
114
  catch (e) {
122
- return textError(errorMessage(e));
115
+ return textError(String(e));
123
116
  }
124
117
  });
118
+ const serverName = "openrouteservice-mcp";
119
+ server.registerTool("health_check", {
120
+ title: "Health check",
121
+ description: "Check if the server is running and responsive.",
122
+ inputSchema: z.object({}),
123
+ annotations: READ_ONLY,
124
+ }, async () => {
125
+ return text("OK - server is running");
126
+ });
127
+ server.registerTool("version_info", {
128
+ title: "Version info",
129
+ description: "Get server version and metadata.",
130
+ inputSchema: z.object({}),
131
+ annotations: READ_ONLY,
132
+ }, async () => {
133
+ return text(JSON.stringify({ name: serverName, version: "1.0.0" }, null, 2));
134
+ });
135
+ server.registerTool("list_tools", {
136
+ title: "List tools",
137
+ description: "List all available tools on this server.",
138
+ inputSchema: z.object({}),
139
+ annotations: READ_ONLY,
140
+ }, async () => {
141
+ return text("See the tools/list response for the full tool list.");
142
+ });
143
+ server.registerTool("echo", {
144
+ title: "Echo",
145
+ description: "Echo back the provided text.",
146
+ inputSchema: z.object({ text: z.string().describe("Text to echo.") }),
147
+ annotations: READ_ONLY,
148
+ }, async (args) => {
149
+ return text(String(args?.text ?? ""));
150
+ });
125
151
  return server;
126
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrouteservice-mcp",
3
- "version": "1.0.2",
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",
package/dist/gateway.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Thin client for the premium gateway.
3
- *
4
- * The premium tools do not run in this package any more. Their bodies live on
5
- * the gateway, which checks a Polar licence key before it does any work, so
6
- * deleting anything here buys nothing: the answer only exists behind the gate.
7
- *
8
- * Set `MCP_LICENSE_KEY` to a Polar licence key. `MCP_GATEWAY_URL` overrides the
9
- * endpoint, which is only useful for testing.
10
- */
11
- /** Raised when a premium call cannot be completed. `message` is safe to show. */
12
- export declare class GatewayError extends Error {
13
- readonly refused: boolean;
14
- constructor(message: string, refused: boolean);
15
- }
16
- export declare function premiumTool(tool: string, args: Record<string, unknown>): Promise<string>;
package/dist/gateway.js DELETED
@@ -1,85 +0,0 @@
1
- /**
2
- * Thin client for the premium gateway.
3
- *
4
- * The premium tools do not run in this package any more. Their bodies live on
5
- * the gateway, which checks a Polar licence key before it does any work, so
6
- * deleting anything here buys nothing: the answer only exists behind the gate.
7
- *
8
- * Set `MCP_LICENSE_KEY` to a Polar licence key. `MCP_GATEWAY_URL` overrides the
9
- * endpoint, which is only useful for testing.
10
- */
11
- const DEFAULT_GATEWAY_URL = 'https://mcp-gateway.contactae2000.workers.dev/mcp';
12
- const DEFAULT_TIMEOUT_MS = 25000;
13
- const USER_AGENT = 'mrfentmen-mcp/1.0';
14
- /** Raised when a premium call cannot be completed. `message` is safe to show. */
15
- export class GatewayError extends Error {
16
- refused;
17
- constructor(message, refused) {
18
- super(message);
19
- this.refused = refused;
20
- this.name = 'GatewayError';
21
- }
22
- }
23
- export async function premiumTool(tool, args) {
24
- const key = (process.env.MCP_LICENSE_KEY ?? '').trim();
25
- if (!key) {
26
- throw new GatewayError(`"${tool}" is a premium tool and no licence key is set. Set MCP_LICENSE_KEY in your MCP client config.`, true);
27
- }
28
- const url = (process.env.MCP_GATEWAY_URL ?? DEFAULT_GATEWAY_URL).trim();
29
- const controller = new AbortController();
30
- const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
31
- let response;
32
- try {
33
- response = await fetch(url, {
34
- method: 'POST',
35
- headers: {
36
- 'content-type': 'application/json',
37
- accept: 'application/json',
38
- 'user-agent': USER_AGENT,
39
- authorization: `Bearer ${key}`,
40
- },
41
- body: JSON.stringify({
42
- jsonrpc: '2.0',
43
- id: 1,
44
- method: 'tools/call',
45
- params: { name: tool, arguments: args },
46
- }),
47
- signal: controller.signal,
48
- });
49
- }
50
- catch {
51
- throw new GatewayError(`"${tool}" could not reach the licence gateway. Check your connection and try again.`, false);
52
- }
53
- finally {
54
- clearTimeout(timer);
55
- }
56
- if (!response.ok) {
57
- throw new GatewayError(`"${tool}" could not reach the licence gateway (HTTP ${response.status}).`, false);
58
- }
59
- let body;
60
- try {
61
- body = (await response.json());
62
- }
63
- catch {
64
- throw new GatewayError(`"${tool}" got an unreadable answer from the licence gateway.`, false);
65
- }
66
- if (body.error) {
67
- throw new GatewayError(`"${tool}" was refused by the licence gateway: ${body.error.message ?? 'unknown error'}`, false);
68
- }
69
- const result = body.result ?? {};
70
- const text = (result.content ?? [])
71
- .map((part) => part.text ?? '')
72
- .join('\n')
73
- .trim();
74
- if (result.isError) {
75
- // The gateway marks a licence refusal structurally, so a missing or rejected
76
- // key is told apart from a tool that failed after the gate let it through.
77
- if (result.structuredContent?.licenceRefusal) {
78
- throw new GatewayError(text || `"${tool}" needs a valid licence key.`, true);
79
- }
80
- throw new GatewayError(text || `"${tool}" failed on the licence gateway.`, false);
81
- }
82
- if (!text)
83
- throw new GatewayError(`"${tool}" returned no data.`, false);
84
- return text;
85
- }
package/dist/license.d.ts DELETED
@@ -1,8 +0,0 @@
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>;