Skip to main content
Glama
ariefrsee

Maritime Vessel Data MCP Server

README.md
# 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](https://modelcontextprotocol.io/) 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 |
|------|-----------------|
| `search_vessels(vessel_type, flag, status, limit)` | which ships match a type, flag state or navigational status |
| `vessels_near_port(port, radius_nm, limit)` | what is within a radius of a named port, nearest first. Radius must be above 0 and at most 500 nautical miles |
| `vessel_details(query)` | 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.

## Quick start

```bash
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:

```bash
python -m maritime_mcp_server.smoke_test
```

It 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](https://aisstream.io), then:

```bash
export AISSTREAM_API_KEY=your-key-here
maritime-mcp-server
```

You 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                 0
```

Nearly 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`:

```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.

```json
"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

```bash
pip install -e ".[dev]"
pytest
```

140 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 | `maritime_mcp_server/server.py` |
| AIS translation and lookup tables | `maritime_mcp_server/ais_mapping.py` |
| Vessel store, merging and expiry | `maritime_mcp_server/store.py` |
| Websocket client and reconnect | `maritime_mcp_server/collector.py` |
| Bundled fallback dataset | `maritime_mcp_server/data/vessels.json` |

Built with the official [`mcp`](https://pypi.org/project/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.py` to cover somewhere other than Malaysia.
- Add ports to `PORT_COORDS` in `server.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](LICENSE).

TDQS

A4.1/5.0

Scored across 14 tools

Disambiguation5/5

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.

Naming Consistency3/5

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.

Tool Count5/5

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.

Completeness5/5

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

ActivityMaintained
ResponsivenessNo issues