@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.
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # gfw-mcp-js
2
+
3
+ Access [Global Fishing Watch](https://globalfishingwatch.org) data from any MCP-compatible AI assistant or directly from the terminal. Search vessels, retrieve fishing and port-visit events, look up Marine Protected Areas, Exclusive Economic Zones and RFMOs, calculate fishing activity hours within any region, and compute aggregate event statistics.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18+
8
+ - A [GFW API key](https://globalfishingwatch.org/our-apis/)
9
+
10
+ ---
11
+
12
+ ## MCP Server
13
+
14
+ ### Quick start (no install)
15
+
16
+ ```bash
17
+ npx gfw-mcp-js mcp
18
+ ```
19
+
20
+ Set your API key via the `GFW_TOKEN` environment variable (or `API_KEY` for compatibility).
21
+
22
+ ### Client configuration
23
+
24
+ #### Claude Desktop
25
+
26
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
27
+ `%APPDATA%\Claude\claude_desktop_config.json` (Windows)
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "gfw": {
33
+ "command": "npx",
34
+ "args": ["-y", "gfw-mcp-js", "mcp"],
35
+ "env": {
36
+ "GFW_TOKEN": "your_gfw_api_key_here"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ #### Claude Code
44
+
45
+ ```bash
46
+ claude mcp add gfw -- npx -y gfw-mcp-js mcp
47
+ export GFW_TOKEN=your_gfw_api_key_here
48
+ ```
49
+
50
+ #### Cursor
51
+
52
+ `.cursor/mcp.json`
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "gfw": {
58
+ "command": "npx",
59
+ "args": ["-y", "gfw-mcp-js", "mcp"],
60
+ "env": { "GFW_TOKEN": "your_gfw_api_key_here" }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ #### Windsurf
67
+
68
+ `~/.codeium/windsurf/mcp_config.json`
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "gfw": {
74
+ "command": "npx",
75
+ "args": ["-y", "gfw-mcp-js", "mcp"],
76
+ "env": { "GFW_TOKEN": "your_gfw_api_key_here" }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ #### VS Code (Copilot)
83
+
84
+ `.vscode/mcp.json`
85
+
86
+ ```json
87
+ {
88
+ "servers": {
89
+ "gfw": {
90
+ "type": "stdio",
91
+ "command": "npx",
92
+ "args": ["-y", "gfw-mcp-js", "mcp"],
93
+ "env": { "GFW_TOKEN": "your_gfw_api_key_here" }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ #### OpenClaw
100
+
101
+ `~/.openclaw/openclaw.json`
102
+
103
+ ```json
104
+ {
105
+ "tools": {
106
+ "mcp": {
107
+ "servers": {
108
+ "gfw": {
109
+ "command": "npx",
110
+ "args": ["-y", "gfw-mcp-js", "mcp"],
111
+ "env": { "GFW_TOKEN": "your_gfw_api_key_here" }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ #### Gemini CLI
120
+
121
+ `~/.gemini/settings.json` (global) or `.gemini/settings.json` (per project)
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "gfw": {
127
+ "command": "npx",
128
+ "args": ["-y", "gfw-mcp-js", "mcp"],
129
+ "env": { "GFW_TOKEN": "your_gfw_api_key_here" }
130
+ }
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### Alternative: local clone
136
+
137
+ ```bash
138
+ git clone https://github.com/globalfishingwatch/gfw-mcp
139
+ cd gfw-mcp
140
+ npm install && npm run build
141
+ ```
142
+
143
+ Then replace `npx -y gfw-mcp-js` with `node /absolute/path/to/gfw-mcp/dist/bin.js` in any config above.
144
+
145
+ ---
146
+
147
+ ## CLI
148
+
149
+ ### Install
150
+
151
+ ```bash
152
+ # Run without installing
153
+ npx gfw-mcp-js --help
154
+
155
+ # Or install globally
156
+ npm install -g gfw-mcp-js
157
+ gfw-mcp-js --help
158
+ ```
159
+
160
+ ### Authentication
161
+
162
+ Token resolution order:
163
+
164
+ 1. `GFW_TOKEN` environment variable
165
+ 2. `API_KEY` environment variable (compatibility alias)
166
+ 3. `~/.gfw/config.json` (saved via `auth login`)
167
+
168
+ ```bash
169
+ # Save token interactively (stored in ~/.gfw/config.json)
170
+ npx gfw-mcp-js auth login
171
+
172
+ # Check which token source is active
173
+ npx gfw-mcp-js auth status
174
+
175
+ # Remove stored token
176
+ npx gfw-mcp-js auth logout
177
+ ```
178
+
179
+ Or pass the token inline for a single command:
180
+
181
+ ```bash
182
+ GFW_TOKEN=your_key npx gfw-mcp-js vessel-search --name "Maria"
183
+ ```
184
+
185
+ ### Commands
186
+
187
+ #### `vessel-search`
188
+
189
+ Search vessels by name, MMSI, IMO, callsign, flag, or activity date range.
190
+
191
+ ```bash
192
+ npx gfw-mcp-js vessel-search --name "Maria" --flag CHN
193
+ npx gfw-mcp-js vessel-search --mmsi 123456789
194
+ npx gfw-mcp-js vessel-search --flag ESP --active-from 2024-01-01 --active-to 2024-12-31 --limit 20
195
+ ```
196
+
197
+ #### `vessel-by-id`
198
+
199
+ Fetch full vessel profile(s) by GFW vessel ID.
200
+
201
+ ```bash
202
+ npx gfw-mcp-js vessel-by-id --ids abc123
203
+ npx gfw-mcp-js vessel-by-id --ids abc123 def456 ghi789
204
+ ```
205
+
206
+ #### `vessel-events`
207
+
208
+ Retrieve fishing, encounter, port visit, or loitering events.
209
+
210
+ ```bash
211
+ npx gfw-mcp-js vessel-events --event-type fishing --start-date 2024-01-01 --end-date 2024-06-01
212
+ npx gfw-mcp-js vessel-events --event-type port_visit --vessel-id abc123 --start-date 2024-01-01 --end-date 2024-12-31
213
+ npx gfw-mcp-js vessel-events --event-type encounter --start-date 2024-01-01 --end-date 2024-12-31 --encounter-types CARRIER-FISHING SUPPORT-FISHING
214
+ npx gfw-mcp-js vessel-events --event-type fishing --region-type EEZ --region-id 8386 --start-date 2024-01-01 --end-date 2024-06-01
215
+ ```
216
+
217
+ #### `events-stats`
218
+
219
+ Compute aggregate event statistics over a date range.
220
+
221
+ ```bash
222
+ npx gfw-mcp-js events-stats --event-type fishing --start-date 2024-01-01 --end-date 2024-12-31
223
+ npx gfw-mcp-js events-stats --event-type fishing --start-date 2024-01-01 --end-date 2024-12-31 --group-by GEARTYPE
224
+ npx gfw-mcp-js events-stats --event-type encounter --start-date 2024-01-01 --end-date 2024-12-31 --region-type RFMO --region-id WCPFC
225
+ ```
226
+
227
+ #### `region-id-lookup`
228
+
229
+ Resolve an MPA, EEZ, or RFMO name to its canonical ID.
230
+
231
+ ```bash
232
+ npx gfw-mcp-js region-id-lookup --region-type MPA --query "Galapagos"
233
+ npx gfw-mcp-js region-id-lookup --region-type EEZ --query "Patagonia" --limit 10
234
+ npx gfw-mcp-js region-id-lookup --region-type RFMO --query "WCPFC"
235
+ ```
236
+
237
+ #### `region-geometry`
238
+
239
+ Get the GeoJSON URL for a specific region.
240
+
241
+ ```bash
242
+ npx gfw-mcp-js region-geometry --region-type EEZ --id 8386
243
+ npx gfw-mcp-js region-geometry --region-type MPA --id 12345
244
+ ```
245
+
246
+ #### `vessel-report`
247
+
248
+ Calculate fishing or presence hours inside a region.
249
+
250
+ ```bash
251
+ npx gfw-mcp-js vessel-report --region-type EEZ --region-id 8386 --start-date 2024-01-01 --end-date 2024-12-31
252
+ npx gfw-mcp-js vessel-report --region-type MPA --region-id 12345 --start-date 2024-01-01 --end-date 2024-12-31 --flags CHN ESP
253
+ npx gfw-mcp-js vessel-report --region-type RFMO --region-id WCPFC --start-date 2024-01-01 --end-date 2024-12-31 --type FISHING --group-by FLAG
254
+ npx gfw-mcp-js vessel-report --region-type EEZ --region-id 8386 --start-date 2024-01-01 --end-date 2024-12-31 --type PRESENCE --vessel-types fishing cargo
255
+ ```
256
+
257
+ ### Output
258
+
259
+ All commands output JSON to stdout, ready to pipe to `jq`:
260
+
261
+ ```bash
262
+ npx gfw-mcp-js vessel-search --name "Maria" | jq '.results[].name'
263
+ npx gfw-mcp-js vessel-report --region-type EEZ --region-id 8386 --start-date 2024-01-01 --end-date 2024-12-31 | jq '.fishingHours'
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Available tools
269
+
270
+ | Tool | Description |
271
+ |------|-------------|
272
+ | `vessel-search` | Search vessels by name, MMSI, IMO, callsign, flag, or gear type |
273
+ | `vessel-by-id` | Fetch full vessel profile(s) by GFW vessel ID(s); returns metadata and a map URL |
274
+ | `vessel-events` | Retrieve fishing, encounter, port visit, or loitering events; filter by vessel, region, date, confidence, and encounter type |
275
+ | `events-stats` | Compute aggregate statistics (total events, unique vessels, flag breakdown) over a date range, optionally filtered by region and grouped by flag or gear type |
276
+ | `region-id-lookup` | Resolve MPA, EEZ, or RFMO names to canonical region IDs |
277
+ | `region-geometry` | Get the URL to fetch the GeoJSON geometry of a specific MPA, EEZ, or RFMO |
278
+ | `vessel-report` | Calculate fishing or presence hours in a region (MPA, EEZ, RFMO) with optional flag, gear type, vessel type, and speed filters; supports groupBy flag/geartype |
279
+
280
+ ---
281
+
282
+ ## Environment variables
283
+
284
+ | Variable | Default | Description |
285
+ |----------|---------|-------------|
286
+ | `GFW_TOKEN` | — | GFW API bearer token |
287
+ | `API_KEY` | — | Alias for `GFW_TOKEN` (backwards compatibility) |
288
+ | `PORT` | `4000` | HTTP port (only used with the optional HTTP transport) |
289
+ | `NODE_ENV` | `development` | Environment name sent to Sentry |
290
+
291
+ ---
292
+
293
+ ## Project structure
294
+
295
+ ```
296
+ bin.ts # Dispatcher: routes to MCP server or CLI
297
+ index.ts # MCP server entry point (stdio transport)
298
+ mcp-server.ts # McpServer creation and tool registration
299
+ cli/
300
+ index.ts # CLI entry point (commander)
301
+ auth.ts # Token resolution and auth commands
302
+ middleware/
303
+ auth.ts # Bearer / X-API-Key authentication middleware
304
+ tools/ # One file per tool; each exports register() + a pure handler
305
+ lib/
306
+ api.ts # gfwFetch() — GFW API client
307
+ response.ts # createToolResponse() / createErrorResponse()
308
+ types.ts # Shared TypeScript types and dataset constants
309
+ ```
package/dist/bin.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Single entry point for the gfw-mcp-js package.
5
+ *
6
+ * npx gfw-mcp-js mcp → start MCP stdio server
7
+ * npx gfw-mcp-js vessel-search --name Maria → CLI one-shot
8
+ * npx gfw-mcp-js --help → CLI help
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const [, , first] = process.argv;
12
+ if (first === 'mcp') {
13
+ // Remove the 'mcp' argument so the MCP entry point sees a clean argv
14
+ process.argv.splice(2, 1);
15
+ import('./index.js').catch((err) => { console.error(err); process.exit(1); });
16
+ }
17
+ else {
18
+ import('./cli/index.js').catch((err) => { console.error(err); process.exit(1); });
19
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveToken = resolveToken;
7
+ exports.authLogin = authLogin;
8
+ exports.authLogout = authLogout;
9
+ exports.authStatus = authStatus;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const readline_1 = __importDefault(require("readline"));
14
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.gfw');
15
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
16
+ function readConfig() {
17
+ try {
18
+ const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
19
+ return JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function writeConfig(config) {
26
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
28
+ }
29
+ /**
30
+ * Resolve the GFW API token with this priority:
31
+ * 1. GFW_TOKEN env var (also accepts API_KEY for compat with the MCP server)
32
+ * 2. ~/.gfw/config.json
33
+ * Throws if no token is found.
34
+ */
35
+ function resolveToken() {
36
+ const token = process.env.GFW_TOKEN ?? process.env.API_KEY ?? readConfig()?.token;
37
+ if (!token) {
38
+ throw new Error('No GFW API token configured.\n' +
39
+ ' Set the GFW_TOKEN env var, or run: gfw auth login');
40
+ }
41
+ return token;
42
+ }
43
+ async function authLogin() {
44
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
45
+ const token = await new Promise((resolve) => {
46
+ rl.question('Enter your GFW API token: ', (answer) => {
47
+ rl.close();
48
+ resolve(answer.trim());
49
+ });
50
+ });
51
+ if (!token) {
52
+ console.error('Token cannot be empty.');
53
+ process.exit(1);
54
+ }
55
+ writeConfig({ token });
56
+ console.log(`Token saved to ${CONFIG_FILE}`);
57
+ }
58
+ function authLogout() {
59
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
60
+ fs_1.default.unlinkSync(CONFIG_FILE);
61
+ console.log('Token removed.');
62
+ }
63
+ else {
64
+ console.log('No token stored.');
65
+ }
66
+ }
67
+ function authStatus() {
68
+ const envToken = process.env.GFW_TOKEN ?? process.env.API_KEY;
69
+ if (envToken) {
70
+ console.log(`Token source: env var (${process.env.GFW_TOKEN ? 'GFW_TOKEN' : 'API_KEY'})`);
71
+ return;
72
+ }
73
+ const config = readConfig();
74
+ if (config?.token) {
75
+ console.log(`Token source: ${CONFIG_FILE}`);
76
+ }
77
+ else {
78
+ console.log('No token configured. Run: gfw auth login');
79
+ }
80
+ }
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const auth_js_1 = require("./auth.js");
6
+ const vessel_search_js_1 = require("../tools/vessel-search.js");
7
+ const vessel_by_id_js_1 = require("../tools/vessel-by-id.js");
8
+ const vessel_events_js_1 = require("../tools/vessel-events.js");
9
+ const events_stats_js_1 = require("../tools/events-stats.js");
10
+ const region_id_lookup_js_1 = require("../tools/region-id-lookup.js");
11
+ const region_geometry_js_1 = require("../tools/region-geometry.js");
12
+ const vessel_report_js_1 = require("../tools/vessel-report.js");
13
+ function print(data) {
14
+ console.log(JSON.stringify(data, null, 2));
15
+ }
16
+ function fail(message) {
17
+ console.error(`Error: ${message}`);
18
+ process.exit(1);
19
+ }
20
+ async function run(fn) {
21
+ try {
22
+ // Inject token into env so gfwFetch picks it up
23
+ process.env.API_KEY = (0, auth_js_1.resolveToken)();
24
+ const result = await fn();
25
+ if (result && typeof result === 'object' && 'isError' in result && result.isError) {
26
+ fail(result.content?.[0]?.text ?? 'Unknown error');
27
+ }
28
+ print(result);
29
+ }
30
+ catch (err) {
31
+ fail(err instanceof Error ? err.message : String(err));
32
+ }
33
+ }
34
+ const program = new commander_1.Command();
35
+ program
36
+ .name('gfw')
37
+ .description('Global Fishing Watch CLI')
38
+ .version('1.0.0');
39
+ // ── auth ──────────────────────────────────────────────────────────────────────
40
+ const auth = program.command('auth').description('Manage GFW API credentials');
41
+ auth.command('login').description('Save a GFW API token').action(auth_js_1.authLogin);
42
+ auth.command('logout').description('Remove stored token').action(auth_js_1.authLogout);
43
+ auth.command('status').description('Show current token source').action(auth_js_1.authStatus);
44
+ // ── vessel-search ─────────────────────────────────────────────────────────────
45
+ program
46
+ .command('vessel-search')
47
+ .description('Search vessels by name, MMSI, IMO, callsign, flag, or date range')
48
+ .option('--name <name>', 'Vessel name or partial name')
49
+ .option('--mmsi <mmsi>', '9-digit MMSI')
50
+ .option('--imo <imo>', '7-digit IMO number')
51
+ .option('--callsign <callsign>', 'Radio callsign')
52
+ .option('--flag <flag>', 'Flag state ISO 3166-1 alpha-3 (e.g. ESP)')
53
+ .option('--active-from <date>', 'Active on or after this date (YYYY-MM-DD)')
54
+ .option('--active-to <date>', 'Active on or before this date (YYYY-MM-DD)')
55
+ .option('--limit <n>', 'Max results (default 10, max 50)', parseInt)
56
+ .action((opts) => run(() => (0, vessel_search_js_1.vesselSearch)({
57
+ name: opts.name,
58
+ mmsi: opts.mmsi,
59
+ imo: opts.imo,
60
+ callsign: opts.callsign,
61
+ flag: opts.flag,
62
+ activeFrom: opts.activeFrom,
63
+ activeTo: opts.activeTo,
64
+ limit: opts.limit,
65
+ })));
66
+ // ── vessel-by-id ──────────────────────────────────────────────────────────────
67
+ program
68
+ .command('vessel-by-id')
69
+ .description('Fetch vessel profile(s) by GFW vessel ID')
70
+ .requiredOption('--ids <ids...>', 'One or more GFW vessel IDs')
71
+ .action((opts) => run(() => (0, vessel_by_id_js_1.vesselById)({ ids: opts.ids })));
72
+ // ── vessel-events ─────────────────────────────────────────────────────────────
73
+ program
74
+ .command('vessel-events')
75
+ .description('Retrieve fishing, encounter, port visit, or loitering events')
76
+ .requiredOption('--event-type <type>', 'Event type: fishing | encounter | port_visit | loitering')
77
+ .requiredOption('--start-date <date>', 'Start date YYYY-MM-DD')
78
+ .requiredOption('--end-date <date>', 'End date YYYY-MM-DD')
79
+ .option('--vessel-id <id>', 'Filter by vessel ID')
80
+ .option('--limit <n>', 'Max results (default 20, max 100)', parseInt)
81
+ .option('--offset <n>', 'Pagination offset', parseInt)
82
+ .option('--confidence <levels...>', 'Confidence levels 2-4 (port_visit only)', (v, acc) => [...acc, parseInt(v)], [])
83
+ .option('--encounter-types <types...>', 'Encounter types (encounter only)')
84
+ .option('--region-type <type>', 'Region type: MPA | EEZ | RFMO')
85
+ .option('--region-id <id>', 'Region canonical ID')
86
+ .action((opts) => run(() => (0, vessel_events_js_1.vesselEvents)({
87
+ eventType: opts.eventType,
88
+ startDate: opts.startDate,
89
+ endDate: opts.endDate,
90
+ vesselId: opts.vesselId,
91
+ limit: opts.limit,
92
+ offset: opts.offset,
93
+ confidence: opts.confidence?.length ? opts.confidence : undefined,
94
+ encounterTypes: opts.encounterTypes?.length ? opts.encounterTypes : undefined,
95
+ regionType: opts.regionType,
96
+ regionId: opts.regionId,
97
+ })));
98
+ // ── events-stats ──────────────────────────────────────────────────────────────
99
+ program
100
+ .command('events-stats')
101
+ .description('Compute aggregate event statistics')
102
+ .requiredOption('--event-type <type>', 'Event type: fishing | encounter | port_visit | loitering')
103
+ .requiredOption('--start-date <date>', 'Start date YYYY-MM-DD')
104
+ .requiredOption('--end-date <date>', 'End date YYYY-MM-DD')
105
+ .option('--confidence <levels...>', 'Confidence levels (port_visit only)')
106
+ .option('--encounter-types <types...>', 'Encounter types (encounter only)')
107
+ .option('--region-type <type>', 'Region type: MPA | EEZ | RFMO')
108
+ .option('--region-id <id>', 'Region canonical ID')
109
+ .option('--group-by <dim>', 'Group by: FLAG | GEARTYPE (default FLAG)')
110
+ .action((opts) => run(() => (0, events_stats_js_1.eventsStats)({
111
+ eventType: opts.eventType,
112
+ startDate: opts.startDate,
113
+ endDate: opts.endDate,
114
+ confidence: opts.confidence?.length ? opts.confidence.map(Number) : undefined,
115
+ encounterTypes: opts.encounterTypes?.length ? opts.encounterTypes : undefined,
116
+ regionType: opts.regionType,
117
+ regionId: opts.regionId,
118
+ groupBy: opts.groupBy,
119
+ })));
120
+ // ── region-id-lookup ──────────────────────────────────────────────────────────
121
+ program
122
+ .command('region-id-lookup')
123
+ .description('Resolve an MPA, EEZ, or RFMO name to its canonical ID')
124
+ .requiredOption('--region-type <type>', 'Region type: MPA | EEZ | RFMO')
125
+ .requiredOption('--query <name>', 'Name or partial name of the region')
126
+ .option('--limit <n>', 'Max results (default 5, max 20)', parseInt)
127
+ .action((opts) => run(() => (0, region_id_lookup_js_1.regionIdLookup)({
128
+ regionType: opts.regionType,
129
+ query: opts.query,
130
+ limit: opts.limit,
131
+ })));
132
+ // ── region-geometry ───────────────────────────────────────────────────────────
133
+ program
134
+ .command('region-geometry')
135
+ .description('Get the GeoJSON URL for an MPA, EEZ, or RFMO')
136
+ .requiredOption('--region-type <type>', 'Region type: MPA | EEZ | RFMO')
137
+ .requiredOption('--id <id>', 'Canonical region ID')
138
+ .action((opts) => {
139
+ // region-geometry is synchronous, no API token needed
140
+ print((0, region_geometry_js_1.regionGeometry)({ regionType: opts.regionType, id: opts.id }));
141
+ });
142
+ // ── vessel-report ─────────────────────────────────────────────────────────────
143
+ program
144
+ .command('vessel-report')
145
+ .description('Calculate fishing or presence hours in a region')
146
+ .requiredOption('--region-type <type>', 'Region type: MPA | EEZ | RFMO')
147
+ .requiredOption('--region-id <id>', 'Canonical region ID')
148
+ .requiredOption('--start-date <date>', 'Start date YYYY-MM-DD')
149
+ .requiredOption('--end-date <date>', 'End date YYYY-MM-DD (exclusive, max 1 year range)')
150
+ .option('--type <type>', 'Activity type: FISHING (default) | PRESENCE')
151
+ .option('--flags <flags...>', 'Flag state ISO 3166-1 alpha-3 codes')
152
+ .option('--vessel-types <types...>', 'Vessel types (PRESENCE only)')
153
+ .option('--speeds <speeds...>', 'Speed ranges (PRESENCE only)')
154
+ .option('--geartypes <geartypes...>', 'Gear types (FISHING only)')
155
+ .option('--group-by <dim>', 'Group by: VESSEL_ID | FLAG | GEARTYPE | FLAGANDGEARTYPE')
156
+ .action((opts) => run(() => (0, vessel_report_js_1.vesselReport)({
157
+ regionType: opts.regionType,
158
+ regionId: opts.regionId,
159
+ startDate: opts.startDate,
160
+ endDate: opts.endDate,
161
+ type: opts.type,
162
+ flags: opts.flags,
163
+ vesselTypes: opts.vesselTypes,
164
+ speeds: opts.speeds,
165
+ geartypes: opts.geartypes,
166
+ groupBy: opts.groupBy,
167
+ })));
168
+ program.parse(process.argv);
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const Sentry = __importStar(require("@sentry/node"));
40
+ const express_1 = __importDefault(require("express"));
41
+ const mcp_server_js_1 = require("./mcp-server.js");
42
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
43
+ Sentry.init({
44
+ dsn: 'https://02861a39640f96d39216f83d54f233cd@o4510353401577472.ingest.us.sentry.io/4511211505057792',
45
+ environment: process.env.NODE_ENV || 'production',
46
+ sendDefaultPii: true,
47
+ });
48
+ const app = (0, express_1.default)();
49
+ app.use(express_1.default.json());
50
+ const server = (0, mcp_server_js_1.createServer)();
51
+ // app.post('/mcp', authenticate, async (req, res) => {
52
+ // const transport = new StreamableHTTPServerTransport({
53
+ // sessionIdGenerator: undefined,
54
+ // enableJsonResponse: true,
55
+ // });
56
+ // res.on('close', () => {
57
+ // transport.close();
58
+ // });
59
+ // await server.connect(transport);
60
+ // await transport.handleRequest(req, res, req.body);
61
+ // });
62
+ // const port = parseInt(process.env.PORT || '4000');
63
+ // app
64
+ // .listen(port, () => {
65
+ // console.error(`Demo MCP Server running on http://localhost:${port}/mcp`);
66
+ // if (API_KEY) {
67
+ // console.error('🔐 Authentication enabled: API key required');
68
+ // } else {
69
+ // console.error(
70
+ // '⚠️ Authentication disabled: No API key configured (set API_KEY or MCP_API_KEY env var)'
71
+ // );
72
+ // }
73
+ // })
74
+ // .on('error', (error) => {
75
+ // console.error('Server error:', error);
76
+ // process.exit(1);
77
+ // });
78
+ async function main() {
79
+ const transport = new stdio_js_1.StdioServerTransport();
80
+ await server.connect(transport);
81
+ console.error('Demo MCP Server running on stdio');
82
+ }
83
+ main().catch((error) => {
84
+ console.error('Fatal error in main():', error);
85
+ Sentry.captureException(error);
86
+ Sentry.flush(2000).finally(() => process.exit(1));
87
+ });
88
+ process.on('uncaughtException', (error) => {
89
+ console.error('Uncaught exception:', error);
90
+ Sentry.captureException(error);
91
+ Sentry.flush(2000).finally(() => process.exit(1));
92
+ });
93
+ process.on('unhandledRejection', (reason) => {
94
+ console.error('Unhandled rejection:', reason);
95
+ Sentry.captureException(reason);
96
+ Sentry.flush(2000).finally(() => process.exit(1));
97
+ });