Maritime Vessel Data MCP Server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Maritime Vessel Data MCP Server Which tankers are near Singapore port?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Maritime Vessel Data MCP Server
Ask an AI assistant "which tankers are anchored near Port Klang?" and get a real answer, from ships that are broadcasting their positions right now.
This is a Model Context Protocol server. It listens to live AIS radio traffic over Malaysian waters and exposes it as standard MCP tools, so any MCP compatible client can ask about vessels without a bespoke integration.
MIT licensed. Python 3.11 or newer. No account needed to try it, and a free API key to run it live.
A real response, abbreviated:
vessels_near_port("Tanjung Pelepas", 40)
{ "data": { "source": "live", "vessel_count": 63, "oldest_position_age_seconds": 140 },
"matches": 27,
"vessels": [
{ "mmsi": "563186500", "name": "ALS CERES", "type": "Cargo", "flag": "Singapore",
"length_m": 255, "status": "Moored", "destination": "MYTTP",
"lat": 1.2612, "lon": 103.7895, "distance_nm": 16.1 } ]}Tools
Tool | What it answers |
| which ships match a type, flag state or navigational status |
| what is within a radius of a named port, nearest first. Radius must be above 0 and at most 500 nautical miles |
| everything known about one ship, by MMSI or name |
Resource vessels://all returns the whole current picture.
Ports: Port Klang, Tanjung Pelepas, Penang, Malacca, Langkawi.
Both list tools take a limit, default 25, between 1 and 200. Every response
reports matches, how many qualified, and returned, how many are included, so
a capped answer says so rather than looking complete. For vessels_near_port the
nearest are kept.
Responses are serialised compactly and positions are rounded to four decimal
places, about 11 metres, which is finer than AIS itself reports. A typical
question costs roughly a fifth of what it did before this was tuned. Fields AIS
has not reported remain explicitly null rather than being dropped, because
absent and unknown are different claims.
Filters are case insensitive, partial, and ignore surrounding whitespace. Bad input is refused rather than answered: a radius outside the allowed range is rejected by the schema before the call runs, and a blank vessel query returns an error asking for a name or MMSI rather than reporting that nothing matched everything.
Related MCP server: @globalfishingwatch/gfw-mcp-js
Quick start
git clone https://github.com/ariefrsee/maritime-mcp-server.git
cd maritime-mcp-server
python3 -m venv .venv && source .venv/bin/activate
pip install .That puts a maritime-mcp-server command on your PATH inside the environment.
Check it works without touching the network:
python -m maritime_mcp_server.smoke_testIt should end with All smoke checks passed.
Going live
Without an API key the server answers from a bundled sample of 18 vessels and says so. Get a free key from aisstream.io, then:
export AISSTREAM_API_KEY=your-key-here
maritime-mcp-serverYou should see one line, AIS stream connected, and then silence. That is
correct: an MCP server over stdio prints no banner and waits for a client.
Give it a minute before asking anything. AIS is a stream, not a database. The server learns about a ship only when that ship transmits, so it starts knowing nothing and fills up over the following minutes. During testing it held 0 vessels at 3 seconds, 40 at 150 seconds and 63 at five minutes. There is no backfill to request; the feed does not replay what you missed.
The subscribed box is 0.5N to 7.5N and 98.5E to 119.5E: the Strait of Malacca, both coasts of the peninsula, and Sabah and Sarawak.
Subscribing to water is not the same as having coverage of it. aisstream is fed by volunteer land-based receivers, and there are very few on the Malaysian coast. Measured over four minutes of live feed, counting distinct vessels within 30 nm of each port:
Tanjung Pelepas 86 Kuching 0
Port Dickson 19 Bintulu 0
Malacca 5 Miri 0
Kota Kinabalu 5 Labuan 0
Port Klang 0 Sandakan 0
Penang 0 Kuantan 0
Langkawi 0 Kemaman 0Nearly everything the server sees is in the Singapore Strait and around Tanjung Pelepas. Port Klang, Malaysia's largest port, returns nothing. Widening the bounding box does not change that; only a receiver near the port, or a paid feed with satellite AIS, would.
Use it from Claude Desktop
Add the server to claude_desktop_config.json. On macOS that lives at
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"maritime-vessel-data": {
"command": "/absolute/path/to/.venv/bin/maritime-mcp-server",
"env": { "AISSTREAM_API_KEY": "your-key-here" }
}
}
}Run pwd in the project directory to get the absolute path. Restart Claude
Desktop fully, then ask it something like "which vessels are near Tanjung
Pelepas right now?" and it will call these tools for you.
Exporting the variable in your terminal does not reach a client launched
process, so it has to go in the env block.
Where the data comes from
Every response opens with a data block naming its source.
"data": { "source": "snapshot", "vessel_count": 18, "snapshot_date": "2026-07-20",
"note": "Live AIS is unavailable, so this is the bundled sample dataset." }source is either live or snapshot. Nothing about how you call the tools
changes between the two, and a snapshot answer is never mistakable for a live
one.
AIS does not transmit everything these tools report. Flag state is derived from
the MMSI country digits, length from the transmitted hull dimensions, and
nearest port is computed here. Anything AIS has not reported is null, never
guessed. A ship broadcasts its position every few seconds and its identity
roughly once in ten of those, so a freshly seen vessel often has a position and
no name, type or size until it next sends static data.
The feed also carries objects that are not ships, such as navigation buoys and base stations. Those are classified by their MMSI prefix and excluded.
Tests
pip install -e ".[dev]"
pytest140 tests, well under a second. No API key, no network, no dependence on the clock. The translation layer runs against 199 real AIS messages captured over the Strait of Malacca and committed as a fixture, so it is checked against traffic that genuinely occurred rather than against invented input.
How it is built
Piece | Where |
MCP tools and the source seam |
|
AIS translation and lookup tables |
|
Vessel store, merging and expiry |
|
Websocket client and reconnect |
|
Bundled fallback dataset |
|
Built with the official mcp SDK, targeting
the 2.x MCPServer API and pinned below 3.0.
Every decision, test result and mistake made while building this is written down
under .shipline/, one folder per piece of work, including the plans, manual
test scripts, retrospectives and runbooks.
Extending it
Widen the bounding box in
collector.pyto cover somewhere other than Malaysia.Add ports to
PORT_COORDSinserver.py.Add tools such as route ETA or anchorage occupancy. Clients discover them automatically.
Switch
run()to the HTTP or SSE transport for remote clients, and add authentication.
Licence
MIT. See LICENSE.
Available Tools
14 toolsanchoragesA
Where vessels actually anchor, found in the recorded positions.
No chart is consulted. This looks for water where a lot of vessels reported "At anchor" and groups the touching parts into one place, so what comes back is where ships really wait rather than where they are permitted to. For a question about queueing, the first is the useful one.
Nothing is named. The cluster off eastern Singapore is obviously the eastern anchorage to anyone who works there, but AIS does not say so and this will not invent it. Each is given its extent, not a radius: an anchorage is usually a long thin thing along a coast and a circle would claim water nobody anchors in.
Bear the coverage caveat in mind. An anchorage nothing is listening to does not appear here, and that is not the same as an anchorage nobody uses.
Inputs: bbox, days, region. Returns: result
congestionA
How busy an area has been, hour by hour, from the recorded track.
Counts distinct vessels per hour by what they were reporting: at anchor, moored, or under way. The anchored count is the one that answers "how long is the queue", which is what a charterer rings an agent about.
Each vessel is counted once per hour it was heard in, so a vessel reporting twenty times in an hour counts once. An hour with no coverage reads as zero and is indistinguishable from an hour with no ships, which is why the response reports how many positions each hour was built from.
Inputs: bbox, hours, region. Returns: result
fleet_trackA
Where every vessel has been over the last few hours.
Returns JSON with a "data" block and a "fleet" list, one entry per vessel, each with its identity and its positions oldest first. Intended for playback and for checking work against what was actually observed.
Each position is one AIS report that was received. The gaps between them are real: the median vessel reports only a handful of times an hour, so two fixes an hour apart are two observations and not a path. Nothing is interpolated.
When "truncated" is true the row cap was reached and this is not the whole picture. Narrow the window rather than assuming the missing vessels are gone.
Inputs: hours, limit. Returns: result
list_regionsA
List the sea areas this server can watch, and what each one costs.
The feed is rationed by throughput rather than by area. A subscription above roughly 25 messages a second is closed by the vendor within a couple of minutes, and a whole-world subscription dies inside one, so a selection has to be chosen to fit a budget rather than simply widened.
Each region reports its measured message rate. A rate of null means nobody has measured that region yet, and a selection containing one has an unknown total: that is reported as unknown rather than as a partial sum. Use set_regions to change what is being watched.
Inputs: no named parameters. Returns: result
passage_timeA
How long this leg actually takes, from ships that made it.
Not distance divided by an assumed speed. That knows nothing about the strait, the traffic, the pilot boarding, or the hour spent waiting for a berth at the far end. This is measured from recorded tracks.
The spread comes with the middle, always. A leg whose median is fourteen hours and whose range is seven to sixty two is not a leg anyone should schedule to fourteen, and reporting the median alone would invite exactly that. Below five observed passages no median is given at all.
Time spent waiting at the origin is not passage time: the clock starts when the vessel leaves the origin's reach, not when it was first seen there. A vessel seen arriving but never departing is not counted, because a record that starts mid-voyage is not evidence of a voyage.
Inputs: origin (required), destination (required), days. Returns: result
port_callsA
When a vessel stopped, where, for how long, and whether she was working.
This is the skeleton of a Statement of Facts, read out of the recorded track rather than typed from memory: arrival, time alongside or at anchor, departure. Anchored and moored are kept apart, because in laytime terms one is waiting and the other is working.
Everything reported was observed. A vessel that has not been seen to leave has no departure time rather than an assumed one; a stop spanning a hole in coverage is cut at the hole rather than claimed as continuous; and a stop far from any known port is reported as at sea rather than given the name of the nearest one. Where an arrival was not witnessed, because the record starts mid-stop or follows a coverage hole, arrival_observed is false and the arrival time is only when this server first saw her there.
Inputs: query (required), days. Returns: result
search_vesselsA
Search the vessel dataset by type, flag state, and/or navigational status.
Any argument left blank is ignored, and surrounding whitespace is ignored. Returns JSON with a "data" block stating the source, a "matches" count of everything that matched, a "returned" count of how many are included, and a "vessels" list. When returned is less than matches the list was capped by the limit argument: raise it or narrow the search to see the rest. When data.source is "snapshot" the positions are from a fixed sample dataset and are not current: say so when answering. Fields that AIS has not reported yet are null, never guessed.
Inputs: flag, limit, status, vessel_type. Returns: result
set_regionsA
Change which sea areas the live subscription watches.
Applied to the open connection where possible, so the feed is not interrupted. Vessels already collected are not discarded: the store keeps them until they age out of its 30-minute window, so a region just switched away from fades rather than vanishing.
A selection over the throughput budget is accepted and reported, not refused. The caller asked for it, and the honest answer is that the feed will drop and reconnect repeatedly rather than a limit that does not exist.
Inputs: region_keys (required). Returns: result
traffic_densityA
Where vessels have actually been, as a grid of counts.
Read this as coverage as much as traffic. It shows where positions were received, so an empty cell means either that no ship went there or that nothing was listening. Port Klang reads as empty on this map and it is one of the busiest ports in the world.
Each cell reports distinct vessels and total positions separately, because they answer different questions: one ship anchored for two days makes hundreds of positions in a single cell, which is a fact about that ship and not about how busy the cell is.
Inputs: bbox, days, region, cell_degrees. Returns: result
vessel_detailsA
Look up a single vessel by exact MMSI or by (partial) name.
Returns JSON with a "data" block stating the source and a "vessel" record, or an error object if no unique match is found. When data.source is "snapshot" the position is not current. Null fields mean AIS has not reported that detail, not that the value is zero or unknown to the vessel.
Inputs: query (required). Returns: result
vessels_in_areaA
Vessels inside one area of sea, rather than everywhere the server watches.
This exists because a selection covering several regions can hold far more vessels than any one caller wants at once, and sending all of them is expensive for an answer about one strait. Ask for the water you care about.
The area filters what has already been collected, which is a different question from what is subscribed to: a region switched away from still has vessels in the store until they age out, and an area nobody is subscribed to simply returns nothing.
Inputs: bbox, limit, region. Returns: result
vessels_near_portA
List vessels within a radius (nautical miles) of a named port.
Recognized ports: Port Klang, Tanjung Pelepas, Penang, Malacca, Langkawi. Returns JSON with a "data" block stating the source, a "matches" count of everything inside the radius, a "returned" count of how many are included, and a "vessels" list annotated with distance_nm, nearest first. When returned is less than matches the nearest were kept and the rest omitted. When data.source is "snapshot" the positions are not current: say so when answering.
Inputs: port (required), limit, radius_nm. Returns: result
vessels_watchedA
Vessels inside the regions currently being watched, and nothing else.
The store keeps every vessel it has collected until the position ages out of the thirty minute window, which includes vessels from a region that has since been deselected. That is the right thing for the store to do and the wrong thing to show: someone who selects Malaysia means show me Malaysia, not Malaysia plus whatever was on screen ten minutes ago.
With no live subscription there is nothing being watched, so no filter applies and everything is returned with a note saying so.
Inputs: limit. Returns: result
vessel_trackA
Where a vessel has been over the last few hours.
Returns JSON with a "data" block and a "track" list of positions, oldest first. Each position is one AIS report that was actually received: the gaps between them are real, and nothing is interpolated to fill them.
History only exists from the point this server started keeping it. An empty track means nothing was heard, not that the vessel did not move.
Inputs: query (required), hours. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 14 tools
Each tool targets a clearly distinct resource or analytical question: vessel search, nearby vessels, area/watched filters, single/fleet tracks, region subscription, and derived metrics like passage time, port calls, congestion, density, and anchorages. Even the geographically overlapping tools are differentiated by their exact filter semantics and described with enough precision to prevent misselection.
Names are readable and consistently snake_case, but the pattern is mixed: some are verb-first (search_vessels, list_regions, set_regions) while most are descriptive noun phrases (vessel_track, port_calls, traffic_density, anchorages). There is no single consistent convention, though the names remain self-explanatory.
Fourteen tools is within the ideal range and each tool earns its place: discovery, details, tracks, region subscription, and specialized maritime analytics are all represented without redundant or filler tools. The count matches the breadth of the domain well.
The server covers the full read-only AIS workflow: finding vessels, retrieving details, viewing current positions and historical tracks, managing region subscriptions, and producing derived insights like passage times, port calls, congestion, traffic density, and anchorages. No obvious dead ends or missing operations are apparent for its stated purpose.
Maintenance
Related MCP Connectors
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Enrich, search, assess, and manage threat intelligence through 80+ typed MCP tools.
The world's public data, one API. License-cleared public data sources for agents, via MCP and REST.
MCP access to the U.S. federal procurement graph: contracts, opportunities, entities, and more.
Related MCP Servers
- AlicenseAqualityCmaintenanceRead-only MCP server for vessel identity lookup, AIS-style positions, tracks, port calls, carrier schedules, vessel schedules, and delay heuristics with source attribution and BYOK provider support.1461 npm8MIT
- AlicenseNot gradedqualityCmaintenanceAccess Global Fishing Watch data from any MCP-compatible AI assistant or terminal: search vessels, retrieve fishing activity and port events, look up marine regions, and compute aggregate statistics.15 npm2Apache 2.0
- AlicenseAqualityDmaintenanceProvides maritime routing capabilities, enabling computation of oceangoing route distances and full routes in GeoJSON format via MCP tools.310MIT

Datalastic MCP Serverofficial
AlicenseNot gradedqualityCmaintenanceEnables real-time vessel tracking, port data, maritime weather, and maritime intelligence through 25 tools, allowing AI clients to query live vessel positions, registry, port info, area searches, weather, and more.MIT