@cyanheads/gdelt-mcp-server
v0.6.0
Published
Search and analyze global news coverage and US television transcripts via the GDELT Project's real-time APIs via MCP. STDIO or Streamable HTTP.
Maintainers
Readme
Public Hosted Server: https://gdelt.caseyjhand.com/mcp
Overview
News and television coverage analysis from the GDELT Project's DOC and TV APIs — the last 3 months of global news in 65+ languages, and US TV transcripts from 2009 through October 2024 across 150+ stations. Search articles and clips, track coverage spikes, analyze tone, and trace how a story propagated across languages and countries. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
| Tool | Description |
|:---|:---|
| gdelt_search_articles | Search the last 3 months of global news coverage (65+ languages) with full-text and filter operators. Fetches up to 250 articles, returns as many as fit a 48,000-byte response, and hands back the date windows to re-query for the rest. |
| gdelt_get_coverage_timeline | Retrieve a time series of coverage volume or average tone for a query. volume_with_articles mode includes top articles per spike timestep, with points to render a timestep's full article list. |
| gdelt_get_tone_distribution | Get a tone histogram (bins ~−30 to +30) showing whether coverage is uniformly negative, bimodal, or clustered near neutral. |
| gdelt_get_coverage_breakdown | Break down coverage volume by source language or source country — a multi-series time series showing geographic propagation. Values are normalized shares of media output, not article counts. |
| gdelt_search_themes | Find GKG theme identifiers for the theme: operator the DOC tools accept — drought finds NATURAL_DISASTER_DROUGHT, cyberattack finds CYBER_ATTACK — ranked by the count GDELT's theme lookup lists. |
| gdelt_search_tv | Search US television news closed captions (2009–Oct 2024) and return per-station airtime time series. |
| gdelt_get_tv_clips | Retrieve matching TV clips with transcript excerpts and Internet Archive viewing links — up to 3,000 fetched, as many as fit a 48,000-byte response returned — and the date windows to re-query for the rest. |
| gdelt_get_tv_context | Get the most frequent co-occurring words and phrases from TV clips matching a query. |
| gdelt_list_tv_stations | List TV stations with market, network, and monitoring date ranges — all of them, or filtered by station ID, network, or market — to verify station availability before querying. |
Capability reference
gdelt_search_articles tool
- Full GDELT query syntax: phrases, boolean OR, exclusion, filter operators (
sourcecountry:,sourcelang:,domain:,theme:,tone</tone>), proximity (near20:) and repetition (repeat3:) —gdelt_search_themesfinds the identifierstheme:takes - Configurable sort (
relevance,dateDesc,dateAsc,toneDesc,toneAsc,hybridRel) and fetch count, up to 250 per call - Returns URL, title, publication date, domain, language, source country, and social image URL
- Each response carries as many fetched articles as fit a 48,000-byte budget on each surface; the rest are counted in
withheldCount, and the notice says how to reach them — never by raisingmaxRecords continuationWindowson a page cut underdateDesc/dateAscholds one window resuming from the last returned article — or skipping past its second when resuming there cannot reach a new article; under the other sorts, and whenevermaxRecordssits at its 250 ceiling, it holds the queried window halved and overlapping by a second so nothing falls through the seam. Articles on a boundary second can come back twice — de-duplicate byurl- Articles GDELT returns from outside an explicit
startDatetime/endDatetimewindow are dropped, and the notice counts them - 250 is a hard per-call ceiling, not a page size — GDELT exposes no cursor, so narrowing the date window is the only way past it
gdelt_get_coverage_timeline tool
- Three modes:
volume(normalized % per timestep),volume_with_articles(volume plus top articles per spike — signal detection in one call),tone(average sentiment per timestep) - Every article reference is always in
structuredContent; the text surface renders the first 3 links per timestep beside that timestep's true count, andpoints: ["<date>"]renders named timesteps in full - Configurable smoothing (0–5 timesteps) and time range (
timespan, or explicitstartDatetime/endDatetime) - Date resolution (
15min/hour/day) is inferred from the returned intervals, and omitted when fewer than two timesteps come back - A
pointsdate matching no timestep is rejected with the available timestep list, rather than silently ignored
gdelt_get_tone_distribution tool
- Histogram bins from approximately −30 to +30; each bin includes representative article URLs
- Summary fields:
peakNegativeBin,peakPositiveBin,neutralPct(% of articles in the −2 to +2 range) - A snapshot across all matching articles — distinct from the tone timeline (
gdelt_get_coverage_timelinemodetone), which is a time series
gdelt_get_coverage_breakdown tool
- Breaks down by
languageorcountryinto a multi-series time series - Top 10 series by total volume; the rest aggregate into
otherAggregated, with every folded-in label named inotherSeriesLabels - Pass any label to
series: ["<label>"]to retrieve that series complete underselectedSeries, ranked or not - Values are normalized shares of media output, not article counts — a high value means the topic dominated that source's coverage, not that it published the most articles
- A
serieslabel matching no series is rejected with the available label list, rather than silently skipped
gdelt_search_themes tool
- Searches the identifiers in GDELT's GKG theme lookup (59,315 themes); the lookup has no labels or descriptions, so a match is the identifier, its listed count, and a paste-ready
operatorsuch astheme:TAX_DISEASE_OUTBREAK - Case-insensitive, leading
theme:ignored; every query word must begin one of an identifier's_/--separated parts or run across consecutive parts (cyberattack→CYBER_ATTACK), or all the words joined must (plant disease→TAX_PLANTDISEASE) - No stemming or synonyms, with one disclosed fallback: when nothing matches, the search retries once with a trailing
sdropped from each word of four or more letters (protests→PROTEST), and the notice says so - Ranked with an exact identifier match first, then by listed count, then by identifier. The count is a static prevalence figure from the lookup, not a live article total
- Pages with
offset/limit(1–100, default 25);nextOffsetappears only when more matches remain, and an offset past a non-empty result failsoffset_out_of_range - The lookup is downloaded on first use and held for the process; a failed download is not kept, so the next call retries it. It is served from a different host than the DOC and TV APIs and does not queue behind their rate limit
gdelt_search_tv tool
- Up to 10 structured
stations(e.g.["CNN", "FOXNEWS"]), or astation:selector embedded in the query — the TV API requires at least one, either way normalizetoggles relative airtime % (default) vs. raw matching 15-second clip counts; optionaldateresaggregation (hour/day/week/month/year)- Responses page at most 500 points per call in deterministic date-then-station order; use
nextOffsetwith the same inputs to retrieve the next page - TV-specific operators:
station:,network:,market:,show:,context: - Verify station active date ranges with
gdelt_list_tv_stationsbefore querying recent events
gdelt_get_tv_clips tool
- Up to 3,000 clips fetched per call, sorted by relevance, date descending, or date ascending
- Each clip: show name, station, air timestamp, 15-second transcript excerpt, direct Archive.org link, and optional thumbnail
- The TV API answers a window in whole clock hours (start floored to the hour, the end's hour included) and rejects one under 30 minutes, so the server requests whole hours — any window width works; an end exactly on the hour leaves out that final second — and drops clips dated outside an explicit
startDatetime/endDatetimewindow, counting them in the notice (atimespancall drops nothing) - Each response carries as many in-window clips as fit a 48,000-byte budget on each surface; the rest are counted in
withheldCount, and the notice says how to reach them. Below the 3,000 ceiling it says to continue atmaxRecords3,000, since each continuation request fetches whole hours and the clips outside its window take slots first continuationWindowson a page cut underdateDesc/dateAscholds one window resuming from the last returned clip (de-duplicate byarchiveUrl) — or skipping past its second when resuming there cannot reach a new clip; underrelevance, and whenevermaxRecordssits at its 3,000 ceiling, it holds the window split in two — on a clock hour when one falls inside it, otherwise at the second on a cut page — with the halves sharing no second- 3,000 is a hard per-call ceiling, not a page size — GDELT exposes no cursor, so narrowing the date window is the only way past it
gdelt_get_tv_context tool
- Returns the most frequent non-stopword terms co-occurring with the query across matching clips
- Relative frequency scores 0–100, where the query term itself scores 100
- Use to identify narrative framing, related concepts, or follow-up search terms
gdelt_list_tv_stations tool
- Returns every station with market, network, monitoring start date, and end date — or only those matching the optional
stations,network, andmarketfilters - Filters match whole values case-insensitively and combine with AND (
network: "FOX"does not matchFOXNEWS);activeCountandtotalCountcount the returned stations - A filter that matches nothing returns an empty list with a notice naming it, and requested station IDs that match no station are named in the notice
- The text surface groups stations as national (every
National*market), international (International,Japan), and local/regional (US cities), each line naming its market isActiveis true when the end date is within the last 24 hours- Use to verify a station was active during a target time period, or to discover valid station IDs for the
stationsparameter on other TV tools
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
GDELT-specific:
- Shared outbound pacer across the DOC and TV tools — one request in flight at 1 req/5s, plus a cooldown gate that any GDELT rate-limit response closes for every queued caller (5s, doubling to 60s, reset by the next success)
- Two service layers (
GdeltDocService,GdeltTvService) mapping clean tool parameters to the DOC and TV API URL conventions, plusGdeltThemeService, which holds the GKG theme lookup in memory once downloaded - TV station filter operators embedded in query strings internally — callers pass structured
stationsarrays, not raw query syntax
Agent-friendly output:
- Query echo on every response — searches return the original query and applied timespan so agents can chain calls without re-deriving parameters
- Discriminated series labels — timeline and breakdown responses carry typed
labelfields ("Volume Intensity","Average Tone", language/country names) rather than positional arrays - Structured station metadata —
isActiveboolean and ISO 8601 date fields let agents reason about TV station availability without parsing date strings - Partial-coverage signals in distribution output —
neutralPct,peakNegativeBin,peakPositiveBinsummary fields let agents branch on sentiment without histogramming the raw bins themselves - Zero matches are a result, not an error — a query GDELT answers with nothing returns the normal output shape with empty arrays and a
noticeon how to broaden it; values that would have to be derived from returned data (dateResolution,timeRange, tone peaks,neutralPct) are omitted rather than defaulted
Getting started
Public Hosted Instance
A public instance is available at https://gdelt.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"gdelt-mcp-server": {
"type": "streamable-http",
"url": "https://gdelt.caseyjhand.com/mcp"
}
}
}Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"gdelt-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/gdelt-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"gdelt-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/gdelt-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with Docker:
{
"mcpServers": {
"gdelt-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/gdelt-mcp-server:latest"
]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- No API key required — GDELT is a free public API.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/gdelt-mcp-server.git- Navigate into the directory:
cd gdelt-mcp-server- Install dependencies:
bun install- Configure environment:
cp .env.example .env
# edit .env if you need to override defaultsConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|:---------|:------------|:--------|
| GDELT_BASE_URL | Override the GDELT API base URL for both DOC and TV APIs. | https://api.gdeltproject.org/api/v2 |
| GDELT_REQUEST_DELAY_MS | Minimum milliseconds between GDELT requests (enforces 1 req/5s limit). | 5300 |
| GDELT_REQUEST_TIMEOUT_MS | Deadline for a single GDELT request. The whole call, retries included, is bounded at twice this value. | 60000 |
| MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
| MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
| MCP_HTTP_ENDPOINT_PATH | HTTP endpoint path where the MCP server is mounted. | /mcp |
| MCP_PUBLIC_URL | Public origin override for TLS-terminating reverse-proxy deployments. | none |
| MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
| MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. createApp() declares stateless in code — this server holds no per-session state — and setting this variable overrides that declaration. | stateless |
| MCP_LOG_LEVEL | Log level (debug, info, warning, error, etc.). | info |
| MCP_GC_PRESSURE_INTERVAL_MS | Opt-in Bun-only forced-GC pressure loop in ms. Try 60000 if heap growth is observed under sustained HTTP load. | 0 (disabled) |
| LOGS_DIR | Directory for log files (Node.js only). Absolute paths are used verbatim; a relative path resolves against the application root. | <app-root>/logs |
| STORAGE_PROVIDER_TYPE | Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1. | in-memory |
| OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security audit bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t gdelt-mcp-server .
docker run --rm -p 3010:3010 gdelt-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/gdelt-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|:----------|:--------|
| src/index.ts | createApp() entry point — registers tools and inits services. |
| src/config | Server-specific environment variable parsing and validation with Zod. |
| src/mcp-server/tools | Tool definitions (*.tool.ts). Nine tools across the DOC and TV APIs and the GKG theme lookup. |
| src/services/gdelt | GdeltDocService and GdeltTvService wrapping the DOC and TV APIs, the shared outbound pacer every DOC and TV call queues behind, and GdeltThemeService for the GKG theme lookup. |
| tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools via the barrels in
src/mcp-server/tools/definitions/index.ts - Wrap GDELT API calls: validate raw JSON → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
