@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 +309 -0
- package/dist/bin.js +19 -0
- package/dist/cli/auth.js +80 -0
- package/dist/cli/index.js +168 -0
- package/dist/index.js +97 -0
- package/dist/lib/api.js +27 -0
- package/dist/lib/map-url-generator.js +68 -0
- package/dist/lib/response.js +17 -0
- package/dist/lib/types.js +14 -0
- package/dist/mcp-server.js +55 -0
- package/dist/middleware/auth.js +26 -0
- package/dist/tools/events-stats.js +139 -0
- package/dist/tools/region-geometry.js +33 -0
- package/dist/tools/region-id-lookup.js +75 -0
- package/dist/tools/vessel-by-id.js +75 -0
- package/dist/tools/vessel-events.js +229 -0
- package/dist/tools/vessel-report.js +259 -0
- package/dist/tools/vessel-search.js +135 -0
- package/package.json +46 -0
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
|
+
}
|
package/dist/cli/auth.js
ADDED
|
@@ -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
|
+
});
|