Server data from the Official MCP Registry
Real shortest sea routes between ports: distance, canals/straits, GeoJSON. UN/LOCODE aware.
About
Real shortest sea routes between ports: distance, canals/straits, GeoJSON. UN/LOCODE aware.
Security Report
Valid MCP server (3 strong, 3 medium validity signals). 1 known CVE in dependencies ⚠️ Package registry links to a different repository than scanned source. Imported from the Official MCP Registry. 1 finding(s) downgraded by scanner intelligence.
11 files analyzed · 2 issues found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
How to Install
Add this to your MCP configuration file:
{
"mcpServers": {
"io-github-mayurrawte-searoute": {
"args": [
"-y",
"@searoute-ts/mcp"
],
"command": "npx"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
searoute-ts
Shortest sea route between any two points on Earth. A TypeScript / JavaScript library for maritime route planning, port-to-port distance, ETA estimation, and shipping-lane visualisation — powered by the 2025 Eurostat maritime network.
npm install searoute-ts
import { seaRoute } from 'searoute-ts';
const route = seaRoute([121.5, 31.0], [4.4, 51.9]);
// Shanghai → Rotterdam → GeoJSON LineString, ~10,664 nm via Suez Canal
🗺️ Try the interactive demo — click two points on a map and see the route, with all options live. (source)
Works from plain JavaScript too — the package ships compiled
.jsplus.d.tsdeclarations. The-tsin the name is for searchability, not a language requirement.
Why searoute-ts
- 🚢 Realistic shipping routes, not great-circle lines through Eurasia.
- 🗺️ Returns GeoJSON — drop straight into Leaflet, Mapbox, deck.gl, MapLibre.
- 🌊 2025 Eurostat marnet with explicit Suez, Panama, Bab-el-Mandeb, Malacca, Gibraltar, Dover, Kiel, Corinth, Bering, Magellan, NW/NE Passage labels.
- 🚫 Canal & strait restrictions — force Cape of Good Hope during a Red Sea disruption with one option.
- 📦 Vessel-draft gating — auto-block Panama (15.2 m), Suez (20.1 m), Kiel (7 m), Corinth (7.3 m) when the vessel exceeds the canal limit.
- 🛤️ K-shortest alternatives —
seaRouteAlternativesreturns the baseline plus up to N realistic alternatives. - 🧭 Multi-leg waypoints —
seaRouteMultifor port rotations and itineraries. - ⏱️ ETA from speed —
speedKnots→durationHours. - 🛠️ Modern toolchain — TypeScript 5, ESM + CJS dual build, types included, Node 18+, zero peer deps.
Quick examples
Basic — shortest route
import { seaRoute } from 'searoute-ts';
const route = seaRoute([-74.04, 40.69], [-0.13, 51.5]); // NYC → London
// route.properties.length // ≈ 3 362 nm
// route.properties.units // 'nauticalmiles'
With ETA and units
seaRoute(shanghai, rotterdam, {
units: 'kilometers',
speedKnots: 22,
});
// → 19 753 km, properties.durationHours ≈ 485 h (≈ 20 days)
Red Sea / Suez disruption — force Cape of Good Hope
seaRoute(shanghai, rotterdam, {
restrictions: ['suez', 'babelmandeb'],
});
// → routes via Cape of Good Hope, ~25 800 km
Vessel-aware — Ultra Large Container Ship
seaRoute(shanghai, newYork, {
vesselDraftMeters: 16, // exceeds Panama's 15.2 m TFW
});
// → Panama auto-blocked, route goes via Suez
Port codes (UN/LOCODE)
import 'searoute-ts/ports'; // enables UN/LOCODE strings on the core API
import { seaRoute } from 'searoute-ts';
seaRoute('CNSHA', 'NLRTM'); // Shanghai → Rotterdam
seaRoute('CNSHA', [4.4, 51.9]); // mixing a code and coordinates is fine too
The ~1 600-port dataset lives behind the searoute-ts/ports subpath so the core
stays lean — importing it registers the resolver. You can also resolve codes
yourself:
import { lookupPort, resolvePort } from 'searoute-ts/ports';
lookupPort('SGSIN'); // → { code, name: 'Singapore', country, coordinates: [lon, lat] }
resolvePort('SGSIN'); // → [103.85, 1.28]
Unknown codes throw UnknownPortError. See Port codes below for provenance.
Load the port dataset from a CDN instead of bundling it
Don't want to bundle the ~135 KB dataset? Fetch it at runtime with loadPorts —
the analog of loadNetwork.
The dataset also ships as a raw dist/ports.json, so jsDelivr/unpkg serve it
versioned for free:
import { seaRoute, loadPorts } from 'searoute-ts';
// Pin a version for reproducibility, or use @latest to always get the newest.
await loadPorts('https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/ports.json');
seaRoute('CNSHA', 'NLRTM'); // works — the fetched dataset is now registered
https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/ports.json # newest
https://cdn.jsdelivr.net/npm/searoute-ts@<version>/dist/ports.json # frozen/immutable
(dist/ports.json ships from the release that adds port codes onward — pin any
version at or after it for reproducibility.)
loadPorts registers the fetched dataset (so code strings resolve) and returns
it. It uses the global fetch (Node ≥18 / browsers); pass { fetch } to override.
Multi-leg / port rotation
import { seaRouteMulti } from 'searoute-ts';
seaRouteMulti(
[shanghai, singapore, mumbai, rotterdam],
{ units: 'kilometers', returnPassages: true },
);
// → one concatenated LineString, total length, union of passages
Alternative routes (Yen-style canal permutation)
import { seaRouteAlternatives } from 'searoute-ts';
const alts = seaRouteAlternatives(shanghai, rotterdam, { k: 4 });
// baseline 19 753 km via Suez
// no-malacca 20 759 km
// no-suez 25 315 km (via Panama)
// no-suez-no-panama 25 845 km (via Cape of Good Hope)
Fetch the network from a URL instead of bundling it (optional)
The network is bundled by default, so seaRoute works offline with zero setup.
If you'd rather not ship the ~1 MB network (e.g. to trim a browser bundle,
or to use an updated network without upgrading the package), fetch it at
runtime and pass it via the existing network option:
import { seaRoute, loadNetwork } from 'searoute-ts';
// CORS-enabled, served from GitHub Pages (or point at your own host / a CDN).
const network = await loadNetwork('https://mayurrawte.github.io/searoute-ts/marnet.json');
const route = seaRoute(shanghai, rotterdam, { network });
Only the fetch is async — seaRoute itself stays synchronous. loadNetwork
uses the global fetch (Node ≥18 and all browsers); pass { fetch } to supply
your own. This is purely opt-in; nothing changes if you don't use it.
Which option should I use?
| Approach | How | Data version | Works offline | Best for |
|---|---|---|---|---|
| Bundled (default) | seaRoute(a, b) — no network | pinned to your installed package | ✅ | Most users; zero config, deterministic |
| Latest via URL | loadNetwork('…/marnet.json') | always the newest hosted | ❌ needs network | Always-current data without upgrading |
| Pinned via CDN | loadNetwork('https://cdn.jsdelivr.net/npm/searoute-ts@2.0.1/…') | frozen (immutable) | ❌ needs network | Reproducible builds |
Versioning the hosted network
You choose the version by choosing the URL:
-
@latest/ rolling — the GitHub Pages URL above always serves the current network. Convenient, but it can change under you. -
Pinned & immutable — because the package is on npm, jsDelivr and unpkg serve every published version automatically, with immutable per-version URLs:
https://cdn.jsdelivr.net/npm/searoute-ts@latest/dist/marnet.json # newest https://cdn.jsdelivr.net/npm/searoute-ts@2/dist/marnet.json # newest 2.x https://cdn.jsdelivr.net/npm/searoute-ts@2.0.1/dist/marnet.json # frozenA pinned URL never changes, so your routes stay reproducible. (These standalone-JSON CDN paths land with the package once the network ships as a separate asset — see issue #10; until then, use the GitHub Pages URL.)
For production, prefer a pinned URL (or just the bundled default) so your distances don't shift when the network is updated.
Higher-resolution networks (optional)
The bundled network is Eurostat's 100 km marnet_plus. Eurostat also
publishes finer resolutions, which give more accurate coastal routing and
shorter-hop fidelity at the cost of a larger download and slightly slower
first-route graph construction. Two moderate resolutions ship as subpath
exports so you only pay for them if you import them:
import { DEFAULT_MARNET } from 'searoute-ts/marnet-20km'; // or 'searoute-ts/marnet-50km'
import { seaRoute } from 'searoute-ts';
seaRoute(origin, destination, { network: DEFAULT_MARNET });
Like the bundled default, each variant ships once as a shared
dist/data/marnet-<res>.cjs asset that both the CJS and ESM builds load at
runtime, so importing a variant doesn't duplicate the network across builds.
| Import | Resolution | Segments | JSON size | gzipped | Coastal accuracy |
|---|---|---|---|---|---|
searoute-ts (bundled default) | 100 km | 9,847 | ~1.3 MB | ~0.18 MB | Baseline — good for global routing |
searoute-ts/marnet-50km | 50 km | 15,498 | ~1.9 MB | ~0.27 MB | Modest step up |
searoute-ts/marnet-20km | 20 km | 29,581 | ~3.6 MB | ~0.51 MB | Noticeably finer coastal hops |
via loadNetwork (see below) | 10 km | 48,301 | ~5.9 MB | ~0.84 MB | High — larger download |
via loadNetwork (see below) | 5 km | 72,478 | ~9.0 MB | ~1.24 MB | Highest — largest download |
The 10 km and 5 km networks are large enough that bundling them would dominate
the install, so they are not shipped in the package. Generate them from the
Eurostat source with scripts/build-marnet.cjs (the script header documents the
GDAL conversion), host the resulting JSON, and load it with
loadNetwork
— or pass any FeatureCollection<LineString> to the network option directly.
Output shape
{
type: 'Feature',
geometry: { type: 'LineString', coordinates: [[lon, lat], ...] },
properties: {
length: number, // in `units`, in-water only
units: 'nauticalmiles' | 'kilometers' | 'miles' | ...,
bbox: [minLon, minLat, maxLon, maxLat],
greatCircleLength: number, // haversine between inputs, same units
detourRatio: number, // routeKm / greatCircleKm
originSnapKm: number, // input → network distance
destinationSnapKm: number,
durationHours?: number, // if `speedKnots` set
passages?: ('suez' | 'panama' | ...)[], // if `returnPassages: true`
ecaKm?: number, // if `emissions` + `searoute-ts/eca` imported
ecaFraction?: number, // ecaKm / length (0–1)
co2eTonnes?: number, // if `emissions` + `vesselClass`/factor
}
}
Full options
seaRoute(origin, destination, {
units: 'nauticalmiles', // any Turf unit
restrictions: ['suez', 'babelmandeb'], // block passages (see table below)
via: ['panama'], // require passages (inverse of restrictions)
allowArctic: false, // default — blocks NWP & NEP
vesselDraftMeters: 15, // auto-restrict canals
speedKnots: 22, // → properties.durationHours
appendOriginDestination: false, // prepend/append raw inputs
returnPassages: true, // populate properties.passages
maxSnapDistanceKm: 50, // SnapFailedError if exceeded
network: customMarnet, // BYO FeatureCollection
antimeridian: 'split', // 'unwrap' | 'split' dateline handling
emissions: true, // → properties.ecaKm / co2eTonnes
vesselClass: 'panamax', // CO₂e estimate class
co2eFactorKgPerKm: 225, // override the class factor
glecInflation: 0.15, // +15% distance for CO₂e (GLEC)
});
Inputs can be [lon, lat] arrays, GeoJSON Feature<Point>, bare Point objects,
or a UN/LOCODE string (e.g. 'CNSHA') once searoute-ts/ports is imported.
Antimeridian (dateline) handling
Routes that cross the ±180° meridian (e.g. Yokohama → LA) come back wrapped to
[-180, 180] by default, which many map renderers draw as a straight streak
across the whole map. Pass antimeridian to get map-ready geometry:
seaRoute(yokohama, la, { antimeridian: 'unwrap' }); // continuous LineString (may exceed ±180)
seaRoute(yokohama, la, { antimeridian: 'split' }); // MultiLineString cut at ±180 (RFC 7946)
'unwrap' shifts longitudes by multiples of 360° so the line never jumps the
dateline (ideal for MapLibre/Leaflet/Deck.gl). 'split' cuts the route into a
MultiLineString at ±180°, keeping every coordinate in range. Both apply to
seaRoute and seaRouteMulti; properties.length is unchanged either way.
Forcing routes through a passage (via)
restrictions blocks a passage; via requires one — the inverse. Use
it to compare explicit routings, e.g. "via Suez" against "via Cape of Good Hope",
or to force a Pacific + Panama routing between Asia and Europe:
seaRoute('CNSHA', 'NLRTM', { via: ['suez'] }); // through Suez (the default)
seaRoute('CNSHA', 'NLRTM', { via: ['panama'] }); // across the Pacific + Panama instead
via accepts the same passage names as restrictions and visits multiple
passages in the order given. It routes origin → passage → destination through
each passage's location using the multi-leg machinery, so it composes with the
other options. A passage named in via is never blocked out from under the
requirement (via: ['northeast'] reaches the Northeast Passage without also
needing allowArctic). Naming the same passage in both via and restrictions
is a contradiction and throws NoRouteError.
Emissions & ECA/SECA reporting
Opt in with emissions: true for two rough estimates on properties:
import 'searoute-ts/eca'; // load the ECA/SECA zones (enables ecaKm)
import { seaRoute } from 'searoute-ts';
const r = seaRoute('CNSHA', 'NLRTM', {
emissions: true,
vesselClass: 'panamax', // → co2eTonnes
});
r.properties.ecaKm; // km of the route inside emission-control zones
r.properties.ecaFraction; // that as a fraction of route length (0–1)
r.properties.co2eTonnes; // rough CO₂e estimate for the voyage
ecaKm— how much of the route lies inside ECA/SECA emission-control areas (Baltic, North Sea, Mediterranean, North American and US Caribbean), which drives fuel-type/cost. The zones ship behind thesearoute-ts/ecasubpath export (to keep the core lean); importing it registers them. They are bounding-box approximations of the IMO MARPOL Annex VI areas — good for estimates, not compliance. Swap in higher-fidelity polygons withregisterEcaZones.co2eTonnes— a deliberately simpledistance × vessel-class factorestimate, not a certified figure. Factors are derived transparently from a representative fuel burn and the IMO HFO CO₂ conversion (seeVESSEL_CLASSES); override withco2eFactorKgPerKm. GLEC recommends inflating shortest-path distance by ~15 % for real-world deviations — passglecInflation: 0.15.
Restrictable passages
The first twelve are natively labelled in the Eurostat marnet (exact match
on the feature's pass attribute). The remaining four are detected via
bounding boxes.
| Name | Type | Notes |
|---|---|---|
suez | native | Suez Canal |
panama | native | Panama Canal |
gibraltar | native | Strait of Gibraltar |
babelmandeb | native | Bab-el-Mandeb (babalmandab alias) |
malacca | native | Malacca Strait |
dover | native | Dover Strait |
kiel | native | Kiel Canal |
corinth | native | Corinth Canal |
bering | native | Bering Strait |
magellan | native | Strait of Magellan |
northwest | native | Northwest Passage (blocked by default) |
northeast | native | Northeast Passage (blocked by default) |
bosporus | bbox | Bosphorus |
ormuz | bbox | Strait of Hormuz |
sunda | bbox | Sunda Strait |
cape_horn | bbox | Cape Horn region |
The Northwest and Northeast Passages are mathematically the shortest path for
many Asia ↔ Europe routes but are ice-blocked most of the year, so they are
blocked by default. Opt in with allowArctic: true.
Validated against industry distances
12 real-world lanes within ±10% of published Searoutes / Sea-Distances figures.
| Lane | searoute-ts | Industry ref. |
|---|---|---|
| Shanghai → Rotterdam (Suez) | 19 753 km | ~19 300 km |
| Singapore → Rotterdam (Suez) | 15 630 km | ~15 500 km |
| Mumbai → Rotterdam (Suez) | 11 918 km | ~11 800 km |
| NY → Rotterdam | 6 227 km | ~6 200 km |
| NY → LA (Panama) | 9 154 km | ~9 100 km |
| Yokohama → LA | 9 145 km | ~8 800 km |
| Singapore → LA (trans-Pacific) | 14 364 km | ~14 300 km |
| Caldera (CL) → Bahía Blanca (AR) | 4 810 km | ~5 180 km |
All checks pass in the test suite.
Errors
SnapFailedError— input cannot be projected onto the network withinmaxSnapDistanceKm. Carries.side: 'origin' | 'destination'and.distanceKm: number.NoRouteError— no path exists between the snapped origin and destination (e.g. all viable canals blocked).
API reference
import {
seaRoute, // single shortest route
seaRouteMulti, // ordered waypoints (multi-leg)
seaRouteAlternatives, // K-shortest alternatives
loadNetwork, // optional: fetch a network from a URL/CDN
CANAL_MAX_DRAFT_M, // { panama: 15.2, suez: 20.1, kiel: 7, corinth: 7.3 }
DEFAULT_MARNET, // bundled FeatureCollection<LineString>
PASSAGE_BBOXES, // passage bbox lookup
clearFinderCache, // drop the PathFinder cache (tests / hot reload)
SnapFailedError,
NoRouteError,
UnknownPortError, // thrown for unresolved UN/LOCODE strings
registerPortResolver, // plug in a custom port dataset
// types
type Passage,
type Antimeridian,
type SeaRouteOptions,
type SeaRouteFeature,
type SeaRouteMultiFeature,
type SeaRouteProperties,
type LoadNetworkOptions,
type MarnetNetwork,
type MarnetProperties,
} from 'searoute-ts';
import {
lookupPort, // UN/LOCODE → { code, name, country, coordinates }
resolvePort, // UN/LOCODE → [lon, lat]
PORTS, // the raw dataset (Record<code, PortRecord>)
PORT_COUNT,
type Port,
type PortRecord,
} from 'searoute-ts/ports';
Port codes (UN/LOCODE)
Origins and destinations may be given as UN/LOCODE strings (e.g. 'CNSHA')
instead of coordinates. The port dataset ships behind the searoute-ts/ports
subpath export, so consumers only pay for it if they use it — importing the
subpath (for any of its exports, or purely for its side effect) registers a
resolver into the core so seaRoute('CNSHA', 'NLRTM') works.
- ~1 600 seaports, keyed by UN/LOCODE (primary codes and aliases).
- Source: marchah/sea-ports (MIT),
itself derived from UN/LOCODE. Regenerate with
scripts/build-ports.cjs. - Coordinates are approximate (port-city granularity) — the routing engine snaps them onto the network anyway, so this is fine for distance/visualisation.
- Unknown or malformed codes throw
UnknownPortError.
Use from an AI agent (MCP)
A companion Model Context Protocol server,
@searoute-ts/mcp (source),
lets AI agents (Claude Desktop, the claude CLI, etc.) compute real sea routes
instead of guessing — asking "how far is Shanghai to Rotterdam by sea, avoiding
Suez?" calls the library directly. It exposes two tools, sea_route and
sea_route_alternatives, and accepts port codes ('CNSHA') or coordinates.
claude mcp add searoute -- npx -y @searoute-ts/mcp
Or add it to any MCP client config:
{
"mcpServers": {
"searoute": {
"command": "npx",
"args": ["-y", "@searoute-ts/mcp"]
}
}
}
See the server's README for the full tool reference.
How it works
A two-page deep-dive (graph data, snapping, Dijkstra, restrictions, antimeridian fix, draft logic, alternatives) is in DOCS.md.
FAQ
Is this for navigation? No. The routes are network paths suitable for visualisation and rough distance/duration estimates, not for piloting ships.
Does it support weather routing? No. For weather-aware routing see VISIR-2.
Why are my Asia→Europe routes going through Bering Strait? They aren't,
by default — the Northwest and Northeast Passages are blocked. Pass
allowArctic: true to enable them.
Can I use my own network? Yes — seaRoute(origin, destination, { network }).
Useful for inland waterways or AIS-derived custom graphs. For higher-resolution
Eurostat data (5/10/20/50 km), see
Higher-resolution networks — 20 km and
50 km ship as subpath exports.
Does it handle the Red Sea / Suez crisis? Yes — pass
restrictions: ['suez', 'babelmandeb'] to force Cape of Good Hope routing.
Is the great-circle distance correct across the antimeridian? Yes — the marnet has been normalised so the Pacific is a connected graph, and all distances use haversine internally.
What's the bundle size? What you import at runtime is small: the core plus
the bundled 100 km marnet (~1.1 MB JSON, shipped once as a shared
dist/data/marnet.cjs asset both builds load, rather than inlined into each).
Tree-shakeable, so the optional searoute-ts/marnet-20km / marnet-50km
networks only load if you import them. They do add to the npm tarball, though —
including them the package is ~1.1 MB packed / ~7 MB unpacked (each variant is a
single shared asset, not duplicated per build). If you need the finer networks
without the install cost, generate and host them and use loadNetwork instead.
Credits
- Maritime network — Eurostat searoute v3.5 (EUPL-1.2). Oak Ridge National Labs Global Shipping Lane Network enriched with European AIS data.
- Dijkstra —
geojson-path-finder@2by Per Liedman. - Inspired by
searoute-py(Apache-2.0). - Original JS port — @johnx25bd.
- Geospatial primitives — Turf.js.
License
MIT © Mayur Rawte
Reviews
No reviews yet
Be the first to review this server!
More Developer Tools MCP Servers
Git
Freeby Modelcontextprotocol · Developer Tools
Read, search, and manipulate Git repositories programmatically
Fetch
Freeby Modelcontextprotocol · Developer Tools
Web content fetching and conversion for efficient LLM usage
Toleno
Freeby Toleno · Developer Tools
Toleno Network MCP Server — Manage your Toleno mining account with Claude AI using natural language.
mcp-creator-python
Freeby mcp-marketplace · Developer Tools
Create, build, and publish Python MCP servers to PyPI — conversationally.
MCP Marketplace
Freeby mcp-marketplace · Developer Tools
Search and install MCP servers from inside your AI client.
MarkItDown
Freeby Microsoft · Content & Media
Convert files (PDF, Word, Excel, images, audio) to Markdown for LLM consumption
