
Connects Claude to the GDELT Project's DOC and TV APIs for searching and analyzing global news coverage and US television transcripts. Nine tools span article search across 65 languages and three months of coverage, time series extraction for volume and tone tracking, histogram analysis for sentiment distribution, and geographic breakdown by country or language. The TV side covers 2009 through October 2024 with clip retrieval, trending topics, contextual vocabulary analysis, and station metadata lookups. Built with shared rate limiting to respect GDELT's 1 request per 5 seconds quota. Use this when you need structured access to large scale media monitoring data or want to trace how stories propagate across languages, countries, and broadcast networks without manually polling APIs.
Search and analyze global news coverage and US television transcripts from the GDELT Project's real-time APIs via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://gdelt.caseyjhand.com/mcp
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.
| 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 get named timesteps' full article lists. Pages by offset within a 48,000-byte response. |
gdelt_get_tone_distribution | Get a tone histogram (bins ~−30 to +30) showing whether coverage is uniformly negative, bimodal, or clustered near neutral. Pages by offset within a 48,000-byte response. |
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. Pages by offset within a 48,000-byte response. |
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 GDELT's relevance-ranked sample of 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. |
gdelt_search_articles toolsourcecountry:, sourcelang:, domain:, theme:, tone</tone>), proximity (near20:) and repetition (repeat3:) — gdelt_search_themes finds the identifiers theme: takesrelevance, dateDesc, dateAsc, toneDesc, toneAsc, hybridRel) and fetch count, up to 250 per call — a maxRecords up to 2,500 runs at 250 and reports the clamp in limitClamp; above 2,500 the call is rejectedwithheldCount, and the notice says how to reach them — never by raising maxRecordscontinuationWindows on a page cut under dateDesc/dateAsc holds 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 whenever maxRecords sits 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 by urlstartDatetime/endDatetime window are dropped, and the notice counts themgdelt_get_coverage_timeline toolvolume (normalized % per timestep), volume_with_articles (volume plus top articles per spike — signal detection in one call), tone (average sentiment per timestep)referenceCount, its true count; points: ["<date>"] returns only the named timesteps, each with every linknextOffset to pass back as offset, and the notice names the timesteps returned out of the total. Each series' pointCount and peak cover the whole series on every pagetimespan, or explicit startDatetime/endDatetime)15min/hour/day) is inferred from the returned intervals, and omitted when fewer than two timesteps come backpoints date matching no timestep is rejected with the available timestep list, rather than silently ignored; an offset past the last timestep is rejected with the valid rangegdelt_get_tone_distribution toolreferenceCount, and bins: [<bin>] returns only the named bins, each with every linkpeakNegativeBin, peakPositiveBin, neutralPct (% of articles in the −2 to +2 range)nextOffset to pass back as offsetbins value matching no bin is rejected with the available bins, rather than silently ignoredgdelt_get_coverage_timeline mode tone), which is a time seriesgdelt_get_coverage_breakdown toollanguage or country into a multi-series time seriesotherSeriesLabelsseries: ["<label>"] to get only those series, complete, under selectedSeries, ranked or nototherSeriesLabels rides on every overview page — and a cut page returns nextOffset to pass back as offsetseries label matching no series is rejected with the available label list, rather than silently skippedgdelt_search_themes tooloperator such as theme:TAX_DISEASE_OUTBREAKtheme: 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)s dropped from each word of four or more letters (protests → PROTEST), and the notice says sooffset / limit (1–100, default 25; a limit up to 1,000 runs at 100 and reports the clamp in limitClamp, and above 1,000 the call is rejected); nextOffset appears only when more matches remain, and an offset past a non-empty result fails offset_out_of_rangegdelt_search_tv toolstations (e.g. ["CNN", "FOXNEWS"]), or a station: selector embedded in the query — the TV API requires at least one, either waynormalize toggles relative airtime % (default) vs. raw matching 15-second clip counts; optional dateres aggregation (hour/day/week/month/year)station, the station ID the stations input takes (FOXNEWS), and stationName, GDELT's label for it (FOX News). IDs come from GDELT's station catalog, requested once per process after the first result that needs one, in a single attempt the call waits on for at most 45 seconds from its start. If the catalog cannot be loaded in that time, every series keeps stationName without station, the notice says so, and the next call tries again; a label the catalog does not list comes back the same way, named in the notice. A series GDELT returns with no label at all has an empty stationName and no station, and the notice says sonextOffset with the same inputs to retrieve the next page. A limit up to 5,000 runs at 500 and reports the clamp in limitClamp; above 5,000 the call is rejectedstation:, network:, market:, show:, context:gdelt_list_tv_stations before querying recent eventsgdelt_get_tv_clips toolmaxRecords up to 30,000 runs at 3,000 and reports the clamp in limitClamp; above 30,000 the call is rejectedstation) and GDELT's label for it (stationName), air timestamp, 15-second transcript excerpt, direct Archive.org link, and optional thumbnail. Station IDs resolve as in gdelt_search_tv, so a series' station passes straight into stations herestartDatetime/endDatetime window, counting them in the notice (a timespan call drops nothing)withheldCount, and the notice says how to reach them. Below the 3,000 ceiling it says to continue at maxRecords 3,000, since each continuation request fetches whole hours and the clips outside its window take slots firstcontinuationWindows on a page cut under dateDesc/dateAsc holds one window resuming from the last returned clip (de-duplicate by archiveUrl) — or skipping past its second when resuming there cannot reach a new clip; under relevance, and whenever maxRecords sits 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 secondgdelt_get_tv_context toolnotice. Compare windows or stations with a separate, narrower call for eachgdelt_list_tv_stations toolstations, network, and market filtersnetwork: "FOX" does not match FOXNEWS); activeCount and totalCount count the returned stationsNational* market), international (International, Japan), and local/regional (US cities), each line naming its marketisActive is true when the end date is within the last 24 hoursstations parameter on other TV toolsBuilt 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:
GdeltDocService, GdeltTvService) mapping clean tool parameters to the DOC and TV API URL conventions, plus GdeltThemeService, which holds the GKG theme lookup in memory once downloadedstations arrays, not raw query syntaxAgent-friendly output:
label fields ("Volume Intensity", "Average Tone", language/country names) rather than positional arraysisActive boolean and ISO 8601 date fields let agents reason about TV station availability without parsing date stringsneutralPct, peakNegativeBin, peakPositiveBin summary fields let agents branch on sentiment without histogramming the raw bins themselvesnotice on how to broaden it; values that would have to be derived from returned data (dateResolution, timeRange, tone peaks, neutralPct) are omitted rather than defaultedA 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"
}
}
}
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/mcp
git clone https://github.com/cyanheads/gdelt-mcp-server.git
cd gdelt-mcp-server
bun install
cp .env.example .env
# edit .env if you need to override defaults
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.
Build and run:
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
Run 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 build -t gdelt-mcp-server .
docker run --rm -p 3010:3010 gdelt-mcp-server
The 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.
| 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/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/tools/definitions/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.