@globalfishingwatch/mcp 0.0.1

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.
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gfwFetch = gfwFetch;
4
+ const GFW_BASE = 'https://gateway.api.globalfishingwatch.org';
5
+ /**
6
+ * Fetch wrapper for the GFW API.
7
+ * Automatically injects the Bearer token from API_KEY env var when present.
8
+ * Throws an Error if the response is not OK.
9
+ */
10
+ async function gfwFetch(path, params) {
11
+ const url = new URL(`${GFW_BASE}${path}`);
12
+ if (params) {
13
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
14
+ }
15
+ const apiKey = process.env.API_KEY;
16
+ console.error(`Making GFW API request to ${url}`);
17
+ const response = await fetch(url.toString(), {
18
+ headers: {
19
+ ...(apiKey && { Authorization: `Bearer ${apiKey}` }),
20
+ Referer: 'gfw-mcp-js',
21
+ },
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(`GFW API error ${response.status}: ${response.statusText}`);
25
+ }
26
+ return response;
27
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateReportUrl = generateReportUrl;
4
+ exports.generateVesselProfileUrl = generateVesselProfileUrl;
5
+ exports.generatePortReportUrl = generatePortReportUrl;
6
+ const qs_1 = require("qs");
7
+ const types_1 = require("./types");
8
+ const DATA_DATAVIEW_INSTANCES = {
9
+ FISHING: 'ais',
10
+ PRESENCE: 'presence',
11
+ };
12
+ const REGIONS_DATAVIEW_INSTANCES = {
13
+ MPA: 'context-layer-mpa',
14
+ EEZ: 'context-layer-eez',
15
+ RFMO: 'context-layer-rfmo',
16
+ };
17
+ const GFW_BASE_URL = 'https://globalfishingwatch.org/map';
18
+ function generateReportUrl(regionId, regionType, type, startDate, endDate, filters = undefined) {
19
+ const baseUrl = '/fishing-activity/default-public';
20
+ let dynamicPath = `/report/${types_1.REGION_DATASETS[regionType]}/${regionId}?reportLoadVessels=true&start=${startDate}&end=${endDate}`;
21
+ const filtersCfg = {};
22
+ const dataviewAIS = {
23
+ id: DATA_DATAVIEW_INSTANCES['FISHING'],
24
+ cfg: {
25
+ vis: type === 'FISHING' ? true : false,
26
+ filters: type === 'FISHING' ? filters : undefined,
27
+ },
28
+ };
29
+ const dataviewPresence = {
30
+ id: DATA_DATAVIEW_INSTANCES['PRESENCE'],
31
+ cfg: {
32
+ vis: type === 'PRESENCE' ? true : false,
33
+ filters: type === 'PRESENCE' ? filters : undefined,
34
+ },
35
+ };
36
+ const dataviewVMS = {
37
+ id: 'vms',
38
+ cfg: {
39
+ vis: false,
40
+ },
41
+ };
42
+ const dataviewRegion = {
43
+ id: REGIONS_DATAVIEW_INSTANCES[regionType],
44
+ cfg: {
45
+ vis: true,
46
+ },
47
+ };
48
+ console.error('object', JSON.stringify({
49
+ dvIn: [dataviewAIS, dataviewVMS, dataviewPresence, dataviewRegion],
50
+ }));
51
+ const dataviewInstances = (0, qs_1.stringify)({ dvIn: [dataviewAIS, dataviewVMS, dataviewPresence, dataviewRegion] }, { arrayFormat: 'indices' });
52
+ dynamicPath += `&${dataviewInstances}`;
53
+ return `${GFW_BASE_URL}${baseUrl}${dynamicPath}`;
54
+ }
55
+ function generateVesselProfileUrl(vesselId, activeFrom, activeTo, events = []) {
56
+ const baseUrl = '/vessel';
57
+ let dynamicPath = `/${vesselId}?`;
58
+ if (activeFrom && activeTo) {
59
+ dynamicPath += `&activeFrom=${activeFrom}&activeTo=${activeTo}`;
60
+ }
61
+ if (events.length > 0) {
62
+ dynamicPath += events.map((e, i) => `vE[${i}]=${e}`).join('&');
63
+ }
64
+ return `${GFW_BASE_URL}${baseUrl}${dynamicPath}`;
65
+ }
66
+ function generatePortReportUrl(portId) {
67
+ return `${GFW_BASE_URL}/fishing-activity/default-public/ports-report/${portId}?portsReportDatasetId=public-global-port-visits-events:latest`;
68
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createToolResponse = createToolResponse;
4
+ exports.createErrorResponse = createErrorResponse;
5
+ function createToolResponse(text, structured) {
6
+ return {
7
+ content: [{ type: 'text', text }],
8
+ structuredContent: structured,
9
+ isError: false,
10
+ };
11
+ }
12
+ function createErrorResponse(message) {
13
+ return {
14
+ content: [{ type: 'text', text: message }],
15
+ isError: true,
16
+ };
17
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ // ── Region datasets ──────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ACTIVITY_DATASETS = exports.REGION_DATASETS = void 0;
5
+ exports.REGION_DATASETS = {
6
+ MPA: 'public-mpa-all',
7
+ EEZ: 'public-eez-areas',
8
+ RFMO: 'public-rfmo',
9
+ };
10
+ // ── Activity datasets ────────────────────────────────────────────────────────
11
+ exports.ACTIVITY_DATASETS = {
12
+ FISHING: 'public-global-fishing-effort:v4.0',
13
+ PRESENCE: 'public-global-presence:v4.0',
14
+ };
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createServer = createServer;
37
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
38
+ const vesselEvents = __importStar(require("./tools/vessel-events.js"));
39
+ const eventsStats = __importStar(require("./tools/events-stats.js"));
40
+ const vesselSearch = __importStar(require("./tools/vessel-search.js"));
41
+ const vesselById = __importStar(require("./tools/vessel-by-id.js"));
42
+ const regionIdLookup = __importStar(require("./tools/region-id-lookup.js"));
43
+ const regionGeometry = __importStar(require("./tools/region-geometry.js"));
44
+ const mpaVesselReport = __importStar(require("./tools/vessel-report.js"));
45
+ function createServer() {
46
+ const server = new mcp_js_1.McpServer({ name: 'gfw', version: '1.0.0' });
47
+ vesselEvents.register(server);
48
+ eventsStats.register(server);
49
+ vesselSearch.register(server);
50
+ regionIdLookup.register(server);
51
+ regionGeometry.register(server);
52
+ vesselById.register(server);
53
+ mpaVesselReport.register(server);
54
+ return server;
55
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authenticate = exports.API_KEY = void 0;
4
+ exports.API_KEY = process.env.API_KEY;
5
+ const AUTH_REQUIRED = !!exports.API_KEY;
6
+ const authenticate = (req, res, next) => {
7
+ if (!AUTH_REQUIRED) {
8
+ return next();
9
+ }
10
+ const authHeader = req.headers.authorization;
11
+ let providedKey;
12
+ if (authHeader && authHeader.startsWith('Bearer ')) {
13
+ providedKey = authHeader.substring(7);
14
+ }
15
+ else {
16
+ providedKey = req.headers['x-api-key'];
17
+ }
18
+ if (!providedKey || providedKey !== exports.API_KEY) {
19
+ return res.status(401).json({
20
+ error: 'Unauthorized',
21
+ message: 'Invalid or missing API key. Provide it via Authorization: Bearer <key> or X-API-Key header.',
22
+ });
23
+ }
24
+ next();
25
+ };
26
+ exports.authenticate = authenticate;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.eventsStats = eventsStats;
4
+ exports.register = register;
5
+ const zod_1 = require("zod");
6
+ const api_js_1 = require("../lib/api.js");
7
+ const response_js_1 = require("../lib/response.js");
8
+ const types_js_1 = require("../lib/types.js");
9
+ const datasetsByType = {
10
+ fishing: 'public-global-fishing-events:latest',
11
+ encounter: 'public-global-encounters-events:latest',
12
+ port_visit: 'public-global-port-visits-events:latest',
13
+ loitering: 'public-global-loitering-events:latest',
14
+ };
15
+ const MAX_FETCH = 500;
16
+ async function eventsStats({ eventType, startDate, endDate, confidence, encounterTypes, regionType, regionId, groupBy = 'FLAG', }) {
17
+ if (confidence !== undefined && eventType !== 'port_visit') {
18
+ return (0, response_js_1.createErrorResponse)('The confidence filter is only valid when eventType is "port_visit".');
19
+ }
20
+ if (encounterTypes !== undefined && eventType !== 'encounter') {
21
+ return (0, response_js_1.createErrorResponse)('The encounterTypes filter is only valid when eventType is "encounter".');
22
+ }
23
+ if ((regionType === undefined) !== (regionId === undefined)) {
24
+ return (0, response_js_1.createErrorResponse)('regionType and regionId must be provided together.');
25
+ }
26
+ const dataset = datasetsByType[eventType];
27
+ const startIso = startDate
28
+ ? `${startDate}T00:00:00.000Z`
29
+ : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
30
+ const endIso = endDate
31
+ ? `${endDate}T23:59:59.999Z`
32
+ : new Date().toISOString();
33
+ const params = {
34
+ 'datasets[0]': dataset,
35
+ 'start-date': startIso,
36
+ 'end-date': endIso,
37
+ 'group-by': groupBy.toUpperCase(),
38
+ 'time-filter-mode': 'START-DATE',
39
+ 'includes[0]': 'EVENTS_GROUPED',
40
+ 'includes[1]': 'TOTAL_COUNT',
41
+ 'timeseries-interval': 'YEAR',
42
+ };
43
+ if (regionType && regionId) {
44
+ params['region-ids[0]'] = regionId;
45
+ params['region-datasets[0]'] = types_js_1.REGION_DATASETS[regionType];
46
+ }
47
+ if (eventType === 'port_visit') {
48
+ const confidenceList = confidence ?? [4];
49
+ confidenceList.forEach((v, i) => {
50
+ params[`confidences[${i}]`] = String(v);
51
+ });
52
+ }
53
+ if (eventType === 'encounter') {
54
+ const encounterTypeList = encounterTypes ?? ['CARRIER-FISHING', 'SUPPORT-FISHING'];
55
+ const expanded = [];
56
+ for (const v of encounterTypeList) {
57
+ expanded.push(v);
58
+ if (v !== 'FISHING-FISHING') {
59
+ expanded.push(v.split('-').reverse().join('-'));
60
+ }
61
+ }
62
+ [...new Set(expanded)].forEach((v, i) => {
63
+ params[`encounter-types[${i}]`] = v;
64
+ });
65
+ }
66
+ const response = await (0, api_js_1.gfwFetch)('/v3/events/stats', params);
67
+ const data = await response.json();
68
+ return data;
69
+ }
70
+ function register(server) {
71
+ server.registerTool('events-stats', {
72
+ title: 'Events Statistics',
73
+ description: 'Compute aggregate statistics for events (fishing, encounters, port visits, loitering) over a date range. Optionally filter by region (MPA, EEZ, RFMO) and group results by flag or gear type. Returns total event count, unique flags, unique vessels, and grouped counts.',
74
+ inputSchema: {
75
+ eventType: zod_1.z
76
+ .enum(['fishing', 'encounter', 'port_visit', 'loitering'])
77
+ .describe('Type of event to analyse.'),
78
+ startDate: zod_1.z
79
+ .string()
80
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use ISO 8601 date format YYYY-MM-DD for startDate.')
81
+ .describe('Only include events on/after this date. If omitted, defaults to one month ago.'),
82
+ endDate: zod_1.z
83
+ .string()
84
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Use ISO 8601 date format YYYY-MM-DD for endDate. IMPORTANT! this date is exclusive.')
85
+ .describe('Only include events on/before this date. If omitted, defaults to today.'),
86
+ confidence: zod_1.z
87
+ .array(zod_1.z.number().int().min(2).max(4))
88
+ .min(1)
89
+ .optional()
90
+ .describe('Confidence levels to include for port visits. Each value must be 2, 3, or 4. Only valid when eventType is "port_visit". ALWAYS default to [4] unless the user explicitly requests other confidence levels.'),
91
+ encounterTypes: zod_1.z
92
+ .array(zod_1.z.enum([
93
+ 'CARRIER-FISHING',
94
+ 'CARRIER-BUNKER',
95
+ 'FISHING-BUNKER',
96
+ 'FISHING-FISHING',
97
+ 'SUPPORT-FISHING',
98
+ ]))
99
+ .min(1)
100
+ .optional()
101
+ .describe('Types of encounters to include. Only valid when eventType is "encounter". ALWAYS default to ["CARRIER-FISHING", "SUPPORT-FISHING"] unless the user explicitly requests other encounter types.'),
102
+ regionType: zod_1.z
103
+ .enum(['MPA', 'EEZ', 'RFMO'])
104
+ .optional()
105
+ .describe('Type of region to filter by: MPA (Marine Protected Area), EEZ (Exclusive Economic Zone), or RFMO (Regional Fisheries Management Organisation). Must be provided together with regionId.'),
106
+ regionId: zod_1.z
107
+ .string()
108
+ .optional()
109
+ .describe('Canonical ID of the region (MPA, EEZ, or RFMO). Use the Region ID Lookup tool if you only have the name. Must be provided together with regionType.'),
110
+ groupBy: zod_1.z
111
+ .enum(['FLAG', 'GEARTYPE'])
112
+ .optional()
113
+ .describe('Dimension to group events by in the "groups" output. Defaults to "flag". ' +
114
+ '"FLAG": counts per vessel flag state. ' +
115
+ '"GEARTYPE": counts per vessel gear type.'),
116
+ },
117
+ outputSchema: {
118
+ flags: zod_1.z
119
+ .array(zod_1.z.string())
120
+ .describe('All unique flag states found in the matching events.'),
121
+ numEvents: zod_1.z
122
+ .number()
123
+ .describe('Total number of matching events fetched (up to 500).'),
124
+ numFlags: zod_1.z.number().describe('Number of unique flag states.'),
125
+ numVessels: zod_1.z.number().describe('Number of unique vessels.'),
126
+ groups: zod_1.z
127
+ .array(zod_1.z.object({ name: zod_1.z.string(), value: zod_1.z.number() }))
128
+ .describe('Counts grouped by the chosen groupBy dimension, sorted descending by value. Empty when groupBy is not specified.'),
129
+ },
130
+ }, async (params) => {
131
+ try {
132
+ const data = await eventsStats(params);
133
+ return (0, response_js_1.createToolResponse)(JSON.stringify(data, null, 2), data);
134
+ }
135
+ catch (err) {
136
+ return (0, response_js_1.createErrorResponse)(`Failed to compute event statistics: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+ });
139
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.regionGeometry = regionGeometry;
4
+ exports.register = register;
5
+ const zod_1 = require("zod");
6
+ const response_js_1 = require("../lib/response.js");
7
+ const types_js_1 = require("../lib/types.js");
8
+ const GFW_BASE = 'https://gateway.api.globalfishingwatch.org';
9
+ function regionGeometry({ regionType, id, }) {
10
+ const dataset = types_js_1.REGION_DATASETS[regionType];
11
+ const url = `${GFW_BASE}/v3/datasets/${dataset}/context-layers/${id}`;
12
+ return { regionType, id, url };
13
+ }
14
+ function register(server) {
15
+ server.registerTool('region-geometry', {
16
+ title: 'Region Geometry URL',
17
+ description: 'Returns the URL where the GeoJSON geometry of a specific Marine Protected Area (MPA), Exclusive Economic Zone (EEZ), or Regional Fisheries Management Organisation (RFMO) can be retrieved. Use region-id-lookup first to obtain the ID.',
18
+ inputSchema: {
19
+ regionType: zod_1.z
20
+ .enum(['MPA', 'EEZ', 'RFMO'])
21
+ .describe('Type of region: MPA (Marine Protected Area), EEZ (Exclusive Economic Zone), or RFMO (Regional Fisheries Management Organisation)'),
22
+ id: zod_1.z.string().describe('Canonical region ID as returned by region-id-lookup'),
23
+ },
24
+ outputSchema: {
25
+ regionType: zod_1.z.enum(['MPA', 'EEZ', 'RFMO']),
26
+ id: zod_1.z.string(),
27
+ url: zod_1.z.string().describe('URL to fetch the GeoJSON geometry of the region'),
28
+ },
29
+ }, async (params) => {
30
+ const output = regionGeometry(params);
31
+ return (0, response_js_1.createToolResponse)(JSON.stringify(output, null, 2), output);
32
+ });
33
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.regionIdLookup = regionIdLookup;
4
+ exports.register = register;
5
+ const zod_1 = require("zod");
6
+ const api_js_1 = require("../lib/api.js");
7
+ const response_js_1 = require("../lib/response.js");
8
+ const types_js_1 = require("../lib/types.js");
9
+ // Returns a simple similarity score: number of query words found in label
10
+ function score(label, query) {
11
+ const lowerLabel = label.toLowerCase();
12
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
13
+ return words.filter((w) => lowerLabel.includes(w)).length;
14
+ }
15
+ async function regionIdLookup({ regionType, query, limit, }) {
16
+ const maxResults = limit ?? 5;
17
+ const dataset = types_js_1.REGION_DATASETS[regionType];
18
+ const response = await (0, api_js_1.gfwFetch)(`/v3/datasets/${dataset}/context-layers`);
19
+ const layers = await response.json();
20
+ const matches = layers
21
+ .map((layer) => ({ layer, s: score(layer.label, query) }))
22
+ .filter(({ s }) => s > 0)
23
+ .sort((a, b) => b.s - a.s)
24
+ .slice(0, maxResults)
25
+ .map(({ layer }) => ({
26
+ id: String(layer.id),
27
+ name: layer.label,
28
+ country: layer.iso3 ?? undefined,
29
+ source: dataset,
30
+ }));
31
+ return { regionType, query, limit: maxResults, matches };
32
+ }
33
+ function register(server) {
34
+ server.registerTool('region-id-lookup', {
35
+ title: 'Region ID Lookup',
36
+ description: 'Retrieve the canonical identifier (ID) for a Marine Protected Area (MPA), Exclusive Economic Zone (EEZ), or Regional Fisheries Management Organisation (RFMO) based on a human-readable name. When more than one match is returned, ask the user which region they meant before proceeding. Use this tool before requesting fishing hours to ensure you pass the correct region ID.',
37
+ inputSchema: {
38
+ regionType: zod_1.z
39
+ .enum(['MPA', 'EEZ', 'RFMO'])
40
+ .describe('Type of region to search: MPA (Marine Protected Area), EEZ (Exclusive Economic Zone), or RFMO (Regional Fisheries Management Organisation)'),
41
+ query: zod_1.z
42
+ .string()
43
+ .describe('Name or partial name of the region. Case-insensitive substring matching is applied.'),
44
+ limit: zod_1.z
45
+ .number()
46
+ .int()
47
+ .min(1)
48
+ .max(20)
49
+ .optional()
50
+ .describe('Maximum number of results to return (default: 5, max: 20).'),
51
+ },
52
+ outputSchema: {
53
+ regionType: zod_1.z.enum(['MPA', 'EEZ', 'RFMO']),
54
+ query: zod_1.z.string(),
55
+ limit: zod_1.z.number(),
56
+ matches: zod_1.z.array(zod_1.z.object({
57
+ id: zod_1.z.string(),
58
+ name: zod_1.z.string(),
59
+ country: zod_1.z
60
+ .string()
61
+ .optional()
62
+ .describe('ISO 3166-1 alpha-3 country code, if available'),
63
+ source: zod_1.z.string().describe('Data source or catalogue identifier'),
64
+ })),
65
+ },
66
+ }, async (params) => {
67
+ try {
68
+ const output = await regionIdLookup(params);
69
+ return (0, response_js_1.createToolResponse)(JSON.stringify(output, null, 2), output);
70
+ }
71
+ catch (err) {
72
+ return (0, response_js_1.createErrorResponse)(`Failed to look up region: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ });
75
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.vesselById = vesselById;
4
+ exports.register = register;
5
+ const zod_1 = require("zod");
6
+ const api_js_1 = require("../lib/api.js");
7
+ const map_url_generator_js_1 = require("../lib/map-url-generator.js");
8
+ const response_js_1 = require("../lib/response.js");
9
+ const DATASET = 'public-global-vessel-identity:v4.0';
10
+ async function vesselById({ ids }) {
11
+ const params = {
12
+ 'datasets[0]': DATASET,
13
+ ...Object.fromEntries(ids.map((id, i) => [`ids[${i}]`, id])),
14
+ };
15
+ const response = await (0, api_js_1.gfwFetch)('/v3/vessels', params);
16
+ const data = await response.json();
17
+ const results = data.entries.map((entry) => {
18
+ const info = entry.selfReportedInfo[0];
19
+ const combined = entry.combinedSourcesInfo[0];
20
+ const vesselId = info?.id ?? combined?.vesselId ?? '';
21
+ const from = info?.transmissionDateFrom;
22
+ const to = info?.transmissionDateTo;
23
+ return {
24
+ vesselId,
25
+ name: info?.shipname,
26
+ mmsi: info?.ssvid,
27
+ imo: info?.imo ?? undefined,
28
+ callsign: info?.callsign ?? undefined,
29
+ flag: info?.flag,
30
+ gearType: combined?.geartypes?.[0]?.name,
31
+ activeFrom: from,
32
+ activeTo: to,
33
+ mapUrl: vesselId ? (0, map_url_generator_js_1.generateVesselProfileUrl)(vesselId, from, to) : null,
34
+ };
35
+ });
36
+ return { total: data.total, results };
37
+ }
38
+ function register(server) {
39
+ server.registerTool('vessel-by-id', {
40
+ title: 'Vessel by ID',
41
+ description: 'Retrieve one or more vessels by their GFW vessel IDs. Returns the same metadata as vessel-search, including a mapUrl for each vessel. Use when you already know the vessel ID(s) and want to fetch their full profile.',
42
+ inputSchema: {
43
+ ids: zod_1.z
44
+ .array(zod_1.z.string().min(1))
45
+ .min(1)
46
+ .describe('List of GFW vessel IDs to look up.'),
47
+ },
48
+ outputSchema: {
49
+ total: zod_1.z.number(),
50
+ results: zod_1.z.array(zod_1.z.object({
51
+ vesselId: zod_1.z.string(),
52
+ name: zod_1.z.string().nullish(),
53
+ mmsi: zod_1.z.string().nullish(),
54
+ imo: zod_1.z.string().nullish(),
55
+ callsign: zod_1.z.string().nullish(),
56
+ flag: zod_1.z.string().nullish(),
57
+ gearType: zod_1.z.string().nullish(),
58
+ activeFrom: zod_1.z.string().nullish(),
59
+ activeTo: zod_1.z.string().nullish(),
60
+ mapUrl: zod_1.z
61
+ .string()
62
+ .nullish()
63
+ .describe("Global Fishing Watch map URL to view this vessel's profile and track. IMPORTANT!! Always share this full link with the user when presenting vessel results."),
64
+ })),
65
+ },
66
+ }, async (params) => {
67
+ try {
68
+ const output = await vesselById(params);
69
+ return (0, response_js_1.createToolResponse)(JSON.stringify(output, null, 2), output);
70
+ }
71
+ catch (err) {
72
+ return (0, response_js_1.createErrorResponse)(`Failed to fetch vessels: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ });
75
+ }