gdelt-mcp
by nadirdev1
README.md
# gdelt-mcp
A small MCP server that gives AI assistants reliable access to the [GDELT DOC 2.0 API](https://blog.gdeltproject.org/gdelt-doc-2-0-api-debuts/) (worldwide online news, 65 languages, rolling 3-month window).
GDELT accepts **one request every 5 seconds per IP address**. Cloud fetchers and AI tools share their outgoing IP addresses with many other users, so their GDELT calls are often refused with `HTTP 429` before they even start. `gdelt-mcp` runs on a machine with its own address and acts as a polite proxy:
- **one queue** for all calls: one GDELT request at a time, at least 6 s apart (configurable, never under 5 s);
- **retries** after a 429 with a global back-off (10 s, then 20 s), and a clear error when GDELT stays unavailable;
- **cache**: 10 minutes for recent windows, 24 hours for windows that ended more than 3 hours ago; identical concurrent calls are merged; a copy up to 2 hours old is served, flagged `stale`, if GDELT keeps failing;
- **compact, exact results**: tables (columns + rows) instead of pages that a model has to summarise;
- **safe inputs**: explicit UTC windows, dates without a timezone are refused, country names and ISO codes mapped to GDELT's names (GDELT's 2-letter codes are FIPS codes: `CH` is China, not Switzerland).
French installation guide: [docs/guide-fr.md](docs/guide-fr.md).
## Tools
| Tool | GDELT mode | What it returns |
|---|---|---|
| `gdelt_search_articles` | ArtList | Articles matching a query in a window: `[seen, domain, lang, country, title, url]`, newest first, up to 250 per call, with `next_end` to page back in time. |
| `gdelt_coverage_timeline` | TimelineVolRaw / TimelineSourceCountry / TimelineLang | Matching article counts per day or hour with totals, or the countries / languages whose media cover the query most. |
| `gdelt_topic_trend` | 2 × TimelineVolRaw | Articles per day in a window versus the `baseline_days` (default 7) before it: ratio, normalized ratio and a label `rising` / `stable` / `falling` / `no_baseline` / `no_data`. |
| `gdelt_topic_summary` | TimelineVolRaw + ArtList × pages (+ TimelineSourceCountry) | A topic's worldwide coverage over whole UTC days, in one call: total and daily articles with a data-gap check against the 7 previous days; the period's articles listed newest first, page by page (250 per GDELT request) up to `max_articles` (default 1000), duplicate URLs and headlines removed; number of source countries; countries ranked by number of articles, exact when `articles_listed.complete` is true (one country can be left out); languages ranked the same way (`top_languages`, all of them, the excluded country's media left out); and the newest headlines to check the query. |
| `gdelt_proxy_status` | none | Requests made, 429s received, cache and queue state, last success and last error. |
Common parameters of the GDELT tools:
- `query` — GDELT syntax: keywords, `"exact phrase"`, `(a OR b)`, `-excluded`. GDELT machine-translates non-English coverage, so English keywords also match articles in other languages.
- `source_country` — outlets based in a country: English or French name (`Germany`, `Royaume-Uni`) or ISO 3166 code (`DE`).
- `source_lang` — original language: name or ISO code (`spanish`, `es`).
- `domain`, `domain_exact` — a single outlet (`domain:` includes subdomains, `domain_exact` uses `domainis:`).
- `start` / `end` (ISO 8601 **with timezone**, within the last 3 months) or `timespan` (`36h`, `3d`, `1w`); default: the last 24 hours. `gdelt_topic_summary` takes `start_date` / `end_date` (whole UTC days, `YYYY-MM-DD`) instead.
- `max_articles` (`gdelt_topic_summary` only, 250 to 2000, default 1000) — how far back the article list is read, in pages of 250, one GDELT request (~6 s) per page. When the period holds more articles than this, `articles_listed.complete` is false, `top_countries` and `top_languages` count the newest `max_articles` only (`stopped_by: "max_articles"`), and the number of countries comes from the source-country timeline instead of the list.
`seen` is the time GDELT first saw an article (UTC). It is usually close to the publication time but is not the publication time. GDELT only provides titles and metadata: open the URL to read an article.
Example result of `gdelt_search_articles` (shortened):
```json
{
"query": "(Brazil OR Brazilian) election sourcecountry:germany",
"window": { "start": "2026-09-19T06:00:00Z", "end": "2026-09-22T06:00:00Z" },
"sort": "date_desc",
"count": 75,
"max_records": 75,
"truncated": true,
"next_end": "2026-09-20T17:45:00Z",
"columns": ["seen", "domain", "lang", "country", "title", "url"],
"rows": [["2026-09-22T05:30:00Z", "example.de", "German", "Germany", "…", "https://…"]],
"source": { "cached": false, "stale": false, "fetched_at": "2026-09-22T06:00:04Z", "gdelt_url": "https://api.gdeltproject.org/…" }
}
```
Errors come back as tool errors with a code: `invalid_input`, `gdelt_query_error` (GDELT's own message, e.g. a missing parenthesis), `gdelt_rate_limited`, `gdelt_unavailable`, `proxy_busy` (with `retry_after_s`).
## Deploy on a server (Ubuntu 22.04 / 24.04)
You need a server with its own public IPv4 address, a host name whose A record points to it (a subdomain of your domain, or a free name such as DuckDNS), and ports 80 and 443 open in the provider's firewall (many cloud providers open only SSH on a new server).
```bash
curl -fsSLO https://raw.githubusercontent.com/nadirdev1/gdelt-mcp/main/deploy/install.sh
sudo bash install.sh mcp.example.org
```
The script installs Docker if needed, then picks a mode (`MODE=standalone` or `MODE=proxy` forces one):
- **standalone**, when ports 80/443 are free: [Caddy](https://caddyserver.com/) runs next to the app and gets the HTTPS certificate itself;
- **proxy**, when a web server already uses 80/443: the app listens on `127.0.0.1` only. For nginx or Apache, the script offers to add a site for the host name (a new file; nothing else is touched) and to get its certificate with certbot; for other servers (hosting panels, Traefik, Caddy on the host) it prints what to add.
It then generates the access key and prints how to connect:
```
URL: https://mcp.example.org/mcp
Header name: X-API-Key
Header value: <key>
```
Add it as a **custom connector** in your MCP client. In Claude: Settings → Connectors → Add custom connector, with the URL above (choose "Continue anyway" if Claude cannot verify the server: it refuses every request without the key), authentication set to none, and a request header `X-API-Key` whose value is the key; Claude stores header values encrypted and never shows them again. `Authorization: Bearer <key>` works too. The key is the secret: anyone who has it can use your GDELT quota.
For clients that cannot send headers, run the installer with `ALLOW_TOKEN_IN_URL=true`: the key is then also accepted in the URL, `https://mcp.example.org/mcp/<key>`, which becomes a secret that shows up on screens and in logs. The web server must then not log request paths for this host (the script's nginx and Apache sites disable access logs; Caddy logs nothing by default).
Useful commands on the server:
```bash
cd /opt/gdelt-mcp
sudo docker compose ps # status
sudo docker compose logs -f app # logs (one JSON line per GDELT request)
sudo grep MCP_TOKEN .env # show the key
```
### Updating
The server never updates itself. To deploy a version, run on the server:
```bash
sudo bash /opt/gdelt-mcp/deploy/update.sh # latest commit of main
sudo bash /opt/gdelt-mcp/deploy/update.sh v0.3.2 # a tag or a commit
```
The script shows the running and the target commits, asks for confirmation, fetches the code, rebuilds the image, restarts the app, checks `/health` and records the deployed commit in `/var/lib/gdelt-mcp/deployed`. The tests run on GitHub Actions for every push: check that they are green before deploying.
For a **private repository**, give the server a read-only deploy key once; both settings are kept in `.env`:
```bash
sudo ssh-keygen -t ed25519 -N '' -f /root/.ssh/gdelt-mcp-deploy # add the .pub as a read-only deploy key on GitHub
sudo REPO_URL=git@github.com:owner/gdelt-mcp.git DEPLOY_KEY=/root/.ssh/gdelt-mcp-deploy bash /opt/gdelt-mcp/deploy/update.sh
```
Servers installed before 0.3.2 had a systemd timer that deployed `main` on its own every 30 minutes; `install.sh` removes it when run again, and `docs/guide-fr.md` (section 5) gives the manual steps.
## Configuration
Environment variables (set in `/opt/gdelt-mcp/.env`, then `sudo docker compose up -d`):
| Variable | Default | Meaning |
|---|---|---|
| `MCP_TOKEN` | required | Access key, sent by clients in the `X-API-Key` header (or `Authorization: Bearer`); 24–128 characters `[A-Za-z0-9_-]`. |
| `ALLOW_TOKEN_IN_URL` | `true` (`install.sh` writes `false`) | Also accept the key in the URL path, `/mcp/<key>`, for clients that cannot send headers. |
| `DOMAIN` | required (Docker) | Host name of the server. |
| `APP_PORT` | `3000` (or the first free port) | Local port the app listens on (`127.0.0.1` only). |
| `COMPOSE_PROFILES` | `caddy` in standalone mode | Written by `install.sh`; absent when another web server is in front. |
| `GDELT_MIN_INTERVAL_MS` | `6000` | Minimum time between two GDELT requests (at least 5000 against the official API). |
| `GDELT_TIMEOUT_MS` | `20000` | Timeout of one GDELT request. |
| `GDELT_CALL_BUDGET_MS` | `35000` | How long a tool call may wait in the queue or back off before giving up. |
| `GDELT_RETRY_DELAY_1_MS`, `GDELT_RETRY_DELAY_2_MS` | `10000`, `20000` | Back-off after the first and second 429 of a call. |
| `CACHE_RECENT_TTL_MS` | `600000` | Cache lifetime for windows ending less than 3 hours ago. |
| `CACHE_SETTLED_TTL_MS` | `86400000` | Cache lifetime for older windows. |
| `CACHE_STALE_MAX_AGE_MS` | `7200000` | Maximum age of a cached copy served when GDELT fails. |
| `CACHE_MAX_ENTRIES` | `300` | Cache size. |
| `PORT`, `HOST` | `3000`, `0.0.0.0` | HTTP listener. |
## Local development
```bash
npm ci
npm test # unit tests + end-to-end tests against a mock GDELT, with MCP clients of both protocol eras
npm run build
ALLOW_NO_TOKEN=true PORT=3000 node dist/index.js # MCP endpoint: http://localhost:3000/mcp
```
Built with the MCP TypeScript SDK v2: stateless Streamable HTTP, serving both 2026-07-28 clients and 2025-era clients (initialize handshake).
## Data and credits
All data comes from [The GDELT Project](https://www.gdeltproject.org/), an open platform whose datasets may be used freely provided GDELT is cited as the source. This proxy respects GDELT's published rate limit; for heavy use, GDELT recommends its bulk datasets instead of the API.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues