What Shiptrail gives an agent
Shiptrail follows some 40,000 ships at a time from open AIS, the radio messages ships send with their name, position, speed and destination. Every ship we hear has a page, and so does every port. Through MCP, the Model Context Protocol, an assistant reads the same answers those pages show, as data it can use: free, with no key, no account and no sign-in.
It suits agentic workflows that start from a ship or a port:
- Where is she now?Find a ship by her name, MMSI or IMO number, then read where she is, how fast she is going, where she is bound and when she expects to arrive.
- Who is coming in?Watch a port's arrivals for a trip, a delivery or a ship you are waiting to see: the ships in port, the ships on the way by their arrival time, and the ships that left in the last 48 hours.
- Fill in a list of shipsGive an agent a list of MMSIs and have it add each ship's type, size, flag, build year and builder, her photograph and her page.
- Answer with a sourceEvery ship and port in an answer carries the address of its page, so an assistant can cite it and a person can follow it to the map.
Four tools, all read only. They answer what the pages show and nothing more: no track of where a ship has been over time, and no replays.
Connect
The endpoint, MCP over Streamable HTTP. No key and no session: every call stands alone.
https://shiptrail.io/mcpClaude Code
claude mcp add --transport http shiptrail https://shiptrail.io/mcpClaude Desktop and claude.ai
In Settings, open Connectors, choose Add custom connector, name it Shiptrail and paste the endpoint. There is nothing to sign in to.
ChatGPT
Add Shiptrail as a custom connector (in developer mode) with the endpoint as its URL and no authentication.
Perplexity
With a Perplexity subscription, open Settings, then Connectors, and add a custom remote connector: paste the endpoint, choose Streamable HTTP and no authentication. Through Perplexity's Agent API, pass the endpoint as an MCP tool on a request.
Cursor and VS Code
In Cursor, add it to .cursor/mcp.json:
{
"mcpServers": {
"shiptrail": { "url": "https://shiptrail.io/mcp" }
}
}In VS Code, add it to .vscode/mcp.json:
{
"servers": {
"shiptrail": { "type": "http", "url": "https://shiptrail.io/mcp" }
}
}Any Streamable HTTP client
Send each JSON-RPC message as a POST with Content-Type: application/json. The answer is one JSON body, never a stream. It speaks protocol versions 2025-11-25, 2025-06-18 and 2025-03-26, and answers an older one with the newest. A GET is refused with 405: there is no stream to open.
curl -s https://shiptrail.io/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'The answer:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "shiptrail",
"title": "Shiptrail",
"version": "1.0.0",
"description": "Live ships and ports from open AIS: find a ship, read everything her page shows, and see who is arriving at and leaving a port.",
"websiteUrl": "https://shiptrail.io/agents"
},
"instructions": "Shiptrail is a free live map of ships, built from AIS broadcasts heard by receivers near coasts. Find a ship with search_ships (a name, MMSI or IMO), then read her with get_ship by MMSI. Find a port with search_ports (a name or UN/LOCODE), then read its arrivals and departures with get_port; each ship on a board can be read with get_ship. Every ship and port carries its page URL: cite it, so a person can follow, and credit Shiptrail (for example \"Data: Shiptrail, shiptrail.io\") wherever you use its answers. Answers change about once a minute, so keep one for a minute rather than asking again. Positions are what ships broadcast and can be minutes old; destinations and ETAs are typed by crews. Not for navigation. A result with isError says what to do next, such as when to try again."
}
}Then call a tool the same way. No session to keep: every call stands alone.
curl -s https://shiptrail.io/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_ship","arguments":{"mmsi":366562000}}}'Workflows
Chains an agent can follow as they are written. Each step is one call.
Where is she now?
- Find hersearch_ships with her name, such as "queen mary 2". Ships on the map now come first; take the MMSI of the one you mean.
- Read herget_ship with that MMSI. Answer from position, voyage.boundFor and voyage.eta, and say when she was last heard (lastHeard).
- Cite her pageGive her url, so the reader can watch her on the map.
Who is arriving at a port?
- Find the portsearch_ports with its name, such as "honolulu", and take its UN/LOCODE (USHNL).
- Read its boardsget_port with that code. arrivals.inPort is who lies there now; arrivals.enRoute is who is coming, soonest first, grouped by etaDay; departures is who left in the last 48 hours.
- Look closerget_ship with the MMSI of any ship on a board, for her photograph, particulars and the ships near her.
Fill in a list of ships
- One call eachget_ship for each MMSI on the list. A batch of up to 20 calls in one POST is answered in order, one at a time.
- Keep what you needtype, lengthM, flag, particulars.built, particulars.builder and url. When dossier says pending, her particulars are still being looked up: ask again in a minute.
- Mind the paceTwenty ships at a time keeps well inside the limit below.
Cite what you found
Quote a position with the time beside it (position.at), say that a destination and an ETA are what her crew typed, and link the ship's or the port's url.
Credit Shiptrail
Shiptrail is free and asks one thing back: wherever an answer, a report, a chart or an app uses what it gives, credit it, for example "Data: Shiptrail, shiptrail.io", beside the ship's or port's link.
Tools
Each tool takes a JSON object and answers with a sentence in words, then the whole answer as JSON (structuredContent, and the same JSON in the last text block). Times are ISO 8601 in UTC, distances nautical miles, lengths metres and speeds knots, the units of AIS. Every ship and port carries its page's address in url.
search_ships
Find ships by name, MMSI or IMO number.
- Input
- queryRequired. A ship's name or part of it, an MMSI or an IMO number. Text, 3 to 40 characters.
- limitOptional. At most this many ships. A whole number from 1 to 20, 10 when left out.
- Returns
- Find ships by name, MMSI (9 digits) or IMO number (7 digits): every ship with a Shiptrail page, any class, on the map now or heard in the last year. Ships on the map now come first. Use get_ship with an MMSI for the whole record.
- Errors
- A query under 3 or over 40 characters is refused with what to send. When the search of ships off the map is busy, the ships on the map still come back, with a note saying when to try again.
- Limit
- 3 of the 120 units each address may spend, which refill at 4 a second.
- Logged
- No record of the call is kept. Its address is held in the web role's memory only until its allowance is full again; a refused call is counted in the web role's log without the address, and a failure inside Shiptrail logs the error, never who asked.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_ships",
"arguments": {
"query": "manoa",
"limit": 3
}
}
}The structured answer (structuredContent). The same comes as JSON in the last text block, after a sentence that says it in words.
{
"query": "manoa",
"ships": [
{
"mmsi": 366562000,
"name": "MANOA",
"imo": 7907984,
"callSign": "KDBG",
"type": "Cargo ship",
"kind": "cargo",
"flag": "United States",
"lengthM": 262,
"onMap": true,
"lastHeard": "2026-11-03T05:16:43.708Z",
"url": "https://shiptrail.io/vessel/366562000-manoa"
},
{
"mmsi": 338123456,
"name": "MANOA MIST",
"type": "Pleasure craft",
"kind": "sail",
"flag": "United States",
"lengthM": 12,
"onMap": true,
"lastHeard": "2026-11-03T05:15:43.708Z",
"url": "https://shiptrail.io/vessel/338123456-manoa-mist"
},
{
"mmsi": 225995571,
"name": "MANOA",
"flag": "Spain",
"onMap": false,
"lastHeard": "2026-10-03T05:27:00.000Z",
"url": "https://shiptrail.io/vessel/225995571-manoa"
}
]
}get_ship
Everything her page shows, by MMSI.
- Input
- mmsiRequired. Her MMSI, 9 digits. A whole number of 9 digits.
- Returns
- Everything Shiptrail's page shows about one ship, by MMSI: identity, size, position, speed, course and status, destination and ETA, where she came from, recent port calls, particulars and registry facts, photo and its credit, a Wikipedia summary, and the ships near her. No history over time.
- Errors
- An MMSI that no ship has sent in the last year is a result with isError that says to check the number or use search_ships. While her photo and particulars are still being looked up, dossier is pending: ask again in a minute.
- Limit
- 2 of the 120 units each address may spend, which refill at 4 a second.
- Logged
- No record of the call is kept. Its address is held in the web role's memory only until its allowance is full again; a refused call is counted in the web role's log without the address, and a failure inside Shiptrail logs the error, never who asked.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_ship",
"arguments": {
"mmsi": 366562000
}
}
}The structured answer (structuredContent). The same comes as JSON in the last text block, after a sentence that says it in words.
{
"mmsi": 366562000,
"name": "MANOA",
"imo": 7907984,
"callSign": "KDBG",
"type": "Cargo ship",
"kind": "cargo",
"flag": "United States",
"aisClass": "A",
"lengthM": 262,
"beamM": 32,
"draughtM": 9.1,
"onMap": true,
"lastHeard": "2026-11-03T05:16:43.708Z",
"position": {
"lat": 21.31196,
"lon": -157.87795,
"at": "2026-11-03T05:16:43.708Z",
"speedKn": 0,
"courseDeg": 169.5,
"headingDeg": 123,
"status": "Moored"
},
"voyage": {
"destination": "USHNL",
"boundFor": {
"locode": "USHNL",
"name": "Honolulu",
"country": "United States",
"url": "https://shiptrail.io/port/ushnl-honolulu"
},
"inPort": {
"locode": "USHNL",
"name": "Honolulu",
"country": "United States",
"url": "https://shiptrail.io/port/ushnl-honolulu"
},
"from": {
"locode": "USLGB",
"name": "Long Beach",
"country": "United States",
"url": "https://shiptrail.io/port/uslgb-long-beach"
},
"departed": "2026-10-27T12:32:00.000Z"
},
"particulars": {
"built": "1982",
"grossTonnage": 41036,
"builder": "Avondale Shipyard",
"registry": "United States",
"portOfRegistry": "Honolulu"
},
"nearby": [
{
"mmsi": 367213090,
"name": "TIGER6",
"type": "Tug",
"kind": "tug",
"flag": "United States",
"distanceNm": 0.3,
"url": "https://shiptrail.io/vessel/367213090-tiger6"
}
],
"dossier": "complete",
"url": "https://shiptrail.io/vessel/366562000-manoa"
}search_ports
Find ports by name or UN/LOCODE.
- Input
- queryRequired. A port's name or part of it, or its UN/LOCODE. Text, 2 to 40 characters.
- limitOptional. At most this many ports. A whole number from 1 to 20, 10 when left out.
- Returns
- Find ports by name or UN/LOCODE. Busy ports come first, with how many ships are in port, en route and lately departed. Use get_port with a UN/LOCODE for its boards.
- Errors
- A query under 2 characters is refused. A port with a page but no ships now comes back without counts.
- Limit
- 1 of the 120 units each address may spend, which refill at 4 a second.
- Logged
- No record of the call is kept. Its address is held in the web role's memory only until its allowance is full again; a refused call is counted in the web role's log without the address, and a failure inside Shiptrail logs the error, never who asked.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_ports",
"arguments": {
"query": "honolulu"
}
}
}The structured answer (structuredContent). The same comes as JSON in the last text block, after a sentence that says it in words.
{
"query": "honolulu",
"ports": [
{
"locode": "USHNL",
"name": "Honolulu",
"country": "United States",
"lat": 21.3,
"lon": -157.85,
"inPort": 41,
"enRoute": 9,
"departed": 17,
"url": "https://shiptrail.io/port/ushnl-honolulu"
}
]
}get_port
A port's arrivals and departures, by UN/LOCODE.
- Input
- locodeRequired. The port's UN/LOCODE, such as USHNL. Text, five letters or digits.
- limitOptional. At most this many rows on each list; the counts are always whole. A whole number from 1 to 500, 100 when left out.
- Returns
- A port's Arrivals board (ships in port now, then ships en route by ETA) and Departures board (ships that left in the last 48 hours), as its Shiptrail page shows them, by UN/LOCODE. Each row carries the ship's MMSI for get_ship.
- Errors
- A code that names no port is a result with isError that says to use search_ports. A busy port's lists are cut to the limit, and truncated says so; the counts are always whole.
- Limit
- 1 of the 120 units each address may spend, which refill at 4 a second.
- Logged
- No record of the call is kept. Its address is held in the web role's memory only until its allowance is full again; a refused call is counted in the web role's log without the address, and a failure inside Shiptrail logs the error, never who asked.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_port",
"arguments": {
"locode": "USHNL",
"limit": 1
}
}
}The structured answer (structuredContent). The same comes as JSON in the last text block, after a sentence that says it in words.
{
"port": {
"locode": "USHNL",
"name": "Honolulu",
"country": "United States",
"lat": 21.3,
"lon": -157.85,
"url": "https://shiptrail.io/port/ushnl-honolulu"
},
"counts": {
"inPort": 2,
"enRoute": 1,
"departed": 1
},
"arrivals": {
"inPort": [
{
"mmsi": 366562000,
"name": "MANOA",
"type": "Cargo ship",
"kind": "cargo",
"flag": "United States",
"status": "Moored",
"speedKn": 0,
"from": {
"locode": "USLGB",
"name": "Long Beach",
"country": "United States",
"url": "https://shiptrail.io/port/uslgb-long-beach"
},
"onMap": true,
"url": "https://shiptrail.io/vessel/366562000-manoa"
}
],
"enRoute": [
{
"mmsi": 311000863,
"name": "PRIDE OF AMERICA",
"type": "Passenger ship",
"kind": "passenger",
"flag": "Bahamas",
"status": "Under way under engine power",
"speedKn": 18.2,
"from": {
"locode": "USOGG",
"name": "Kahului",
"country": "United States",
"url": "https://shiptrail.io/port/usogg-kahului"
},
"eta": "2026-11-04T07:00:00.000Z",
"etaDay": "2026-11-04",
"onMap": true,
"url": "https://shiptrail.io/vessel/311000863-pride-of-america"
}
]
},
"departures": [
{
"mmsi": 367123450,
"name": "NATOMA",
"type": "Tug",
"kind": "tug",
"flag": "United States",
"boundFor": {
"locode": "USNIJ",
"name": "Nawiliwili",
"country": "United States",
"url": "https://shiptrail.io/port/usnij-nawiliwili"
},
"departed": "2026-11-02T20:00:00.000Z",
"lastHeard": "2026-11-02T22:00:00.000Z",
"onMap": false,
"url": "https://shiptrail.io/vessel/367123450-natoma"
}
],
"truncated": true,
"url": "https://shiptrail.io/port/ushnl-honolulu"
}Good citizenship
- The limitEach address may spend 120 units, then 4 a second: search_ships takes 3, get_ship 2, search_ports and get_port 1 each. Listing the tools, initialize and ping take none.
- When a call says isErrorRead its sentence: it says what to do, such as to check a number or to try again in so many seconds. Wait that long, then ask once more.
- Keep an answer a minuteShips report every few seconds to few minutes, and Shiptrail's lists move about once a minute. Asking again sooner brings back the same answer.
- Link the pageGive the url with what you quote, so a person can see the ship on the map.
- Not for navigationPositions are what ships broadcast, relayed by receivers on land, and can be minutes old; a ship far from any coast is out of range until she nears land again. Destinations and ETAs are what crews type, and can be out of date.
- What is keptNo tool call is logged, and a ship search is counted as a person's is. Privacy and terms has the whole of it, and the terms of use.
Without MCP
The pages are written for agents too. A ship's page (/vessel/ and her MMSI) and a port's (/port/ and its UN/LOCODE) are whole HTML with every fact as text, and carry structured data. The sitemap lists every page worth reading, and llms.txt says what is where.
/api/ is not a public API: it serves the map, changes with it and is closed to crawlers. Use the tools above, or the pages.
