@unclick/gdelt-mcp 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ Apache License 2.0
2
+
3
+ Copyright (c) 2026 UnClick / malamutemayhem
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # GDELT News MCP by UnClick
2
+
3
+ Global news search, events, tone, themes, and geographic signals from GDELT.
4
+
5
+ > By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
6
+
7
+ ## Install
8
+
9
+ Installs straight from GitHub, no npm account needed.
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "gdelt": {
15
+ "command": "npx",
16
+ "args": ["-y", "https://github.com/malamutemayhem/unclick/releases/download/standalone-mcps-latest/gdelt.tgz"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## Tools
23
+
24
+ - `gdelt_news_search`
25
+ - `gdelt_tone_analysis`
26
+ - `gdelt_geo_events`
27
+ - `gdelt_trending`
28
+
29
+ ## Want the rest?
30
+
31
+ This is one connector. [UnClick](https://unclick.world) bundles 180+ tools plus
32
+ persistent cross-session agent memory in a single install.
33
+
34
+ ## License
35
+
36
+ Apache-2.0
@@ -0,0 +1,176 @@
1
+ // GDELT Project integration - global news intelligence, no auth required.
2
+ // Docs: https://www.gdeltproject.org/
3
+ // DOC API: https://api.gdeltproject.org/api/v2/doc/doc
4
+ // GEO API: https://api.gdeltproject.org/api/v2/geo/geo
5
+ // Updated every 15 minutes. Covers all broadcast, print, and web news globally.
6
+ const GDELT_DOC = "https://api.gdeltproject.org/api/v2/doc/doc";
7
+ const GDELT_GEO = "https://api.gdeltproject.org/api/v2/geo/geo";
8
+ // ─── API helpers ──────────────────────────────────────────────────────────────
9
+ async function gdeltFetch(base, params) {
10
+ const url = `${base}?${params}`;
11
+ const res = await fetch(url, {
12
+ headers: { "User-Agent": "UnClickMCP/1.0 (https://unclick.io)" },
13
+ });
14
+ if (!res.ok)
15
+ throw new Error(`GDELT API HTTP ${res.status}`);
16
+ return res.json();
17
+ }
18
+ // ─── gdelt_news_search ────────────────────────────────────────────────────────
19
+ export async function gdeltNewsSearch(args) {
20
+ const query = String(args.query ?? "").trim();
21
+ if (!query)
22
+ return { error: "query is required." };
23
+ const maxrecords = Math.min(250, Math.max(1, Number(args.maxrecords ?? 25)));
24
+ const params = new URLSearchParams({
25
+ query,
26
+ mode: "artlist",
27
+ format: "json",
28
+ maxrecords: String(maxrecords),
29
+ });
30
+ if (args.startdatetime)
31
+ params.set("startdatetime", String(args.startdatetime));
32
+ if (args.enddatetime)
33
+ params.set("enddatetime", String(args.enddatetime));
34
+ if (args.sourcelang)
35
+ params.set("sourcelang", String(args.sourcelang));
36
+ if (args.sourcecountry)
37
+ params.set("sourcecountry", String(args.sourcecountry));
38
+ const data = await gdeltFetch(GDELT_DOC, params);
39
+ const articles = data?.articles ?? [];
40
+ return {
41
+ query,
42
+ count: articles.length,
43
+ articles: articles.map((a) => ({
44
+ title: a.title ?? null,
45
+ url: a.url ?? null,
46
+ domain: a.domain ?? null,
47
+ date: a.seendate ?? null,
48
+ language: a.language ?? null,
49
+ country: a.sourcecountry ?? null,
50
+ image: a.socialimage ?? null,
51
+ })),
52
+ };
53
+ }
54
+ // ─── gdelt_tone_analysis ──────────────────────────────────────────────────────
55
+ // Uses timelinetone mode - returns average tone score over time.
56
+ // Negative values = negative sentiment; positive = positive sentiment.
57
+ export async function gdeltToneAnalysis(args) {
58
+ const query = String(args.query ?? "").trim();
59
+ if (!query)
60
+ return { error: "query is required." };
61
+ const params = new URLSearchParams({
62
+ query,
63
+ mode: "timelinetone",
64
+ format: "json",
65
+ });
66
+ if (args.timespan)
67
+ params.set("timespan", String(args.timespan));
68
+ if (args.sourcelang)
69
+ params.set("sourcelang", String(args.sourcelang));
70
+ if (args.sourcecountry)
71
+ params.set("sourcecountry", String(args.sourcecountry));
72
+ const data = await gdeltFetch(GDELT_DOC, params);
73
+ const timelineArr = data?.timeline ?? [];
74
+ const entries = timelineArr.flatMap((t) => t?.data ?? []);
75
+ if (entries.length === 0) {
76
+ return { query, timeline: [], summary: null };
77
+ }
78
+ const values = entries.map((e) => e.value ?? 0).filter((v) => !isNaN(v));
79
+ const avg = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
80
+ const min = Math.min(...values);
81
+ const max = Math.max(...values);
82
+ const latest = entries[entries.length - 1];
83
+ return {
84
+ query,
85
+ summary: {
86
+ average_tone: Math.round(avg * 100) / 100,
87
+ min_tone: Math.round(min * 100) / 100,
88
+ max_tone: Math.round(max * 100) / 100,
89
+ latest_tone: Math.round((latest?.value ?? 0) * 100) / 100,
90
+ latest_date: latest?.date ?? null,
91
+ interpretation: avg < -2 ? "strongly negative" :
92
+ avg < -0.5 ? "slightly negative" :
93
+ avg < 0.5 ? "neutral" :
94
+ avg < 2 ? "slightly positive" : "strongly positive",
95
+ },
96
+ timeline: entries.slice(-30).map((e) => ({
97
+ date: e.date ?? null,
98
+ tone: Math.round((e.value ?? 0) * 100) / 100,
99
+ })),
100
+ };
101
+ }
102
+ // ─── gdelt_geo_events ─────────────────────────────────────────────────────────
103
+ // Uses the GDELT GEO API - returns geographic event clusters with article counts and tone.
104
+ export async function gdeltGeoEvents(args) {
105
+ const query = String(args.query ?? "").trim();
106
+ if (!query)
107
+ return { error: "query is required." };
108
+ const maxpoints = Math.min(250, Math.max(1, Number(args.maxpoints ?? 50)));
109
+ const params = new URLSearchParams({
110
+ query,
111
+ format: "json",
112
+ maxpoints: String(maxpoints),
113
+ });
114
+ if (args.timespan)
115
+ params.set("timespan", String(args.timespan));
116
+ const data = await gdeltFetch(GDELT_GEO, params);
117
+ const features = data?.features ?? [];
118
+ return {
119
+ query,
120
+ count: features.length,
121
+ events: features.map((f) => ({
122
+ name: f.properties?.name ?? null,
123
+ country: f.properties?.countrycode ?? null,
124
+ coordinates: f.geometry?.coordinates
125
+ ? { lat: f.geometry.coordinates[1], lng: f.geometry.coordinates[0] }
126
+ : null,
127
+ article_count: f.properties?.count ?? 0,
128
+ tone: f.properties?.tone != null
129
+ ? Math.round(f.properties.tone * 100) / 100
130
+ : null,
131
+ })).sort((a, b) => (b.article_count ?? 0) - (a.article_count ?? 0)),
132
+ };
133
+ }
134
+ // ─── gdelt_trending ───────────────────────────────────────────────────────────
135
+ // Uses timelinevol mode - returns article volume over time for a query.
136
+ // Shows whether a topic is surging, stable, or fading in the news cycle.
137
+ export async function gdeltTrending(args) {
138
+ const query = String(args.query ?? "").trim();
139
+ if (!query)
140
+ return { error: "query is required." };
141
+ const params = new URLSearchParams({
142
+ query,
143
+ mode: "timelinevol",
144
+ format: "json",
145
+ });
146
+ if (args.timespan)
147
+ params.set("timespan", String(args.timespan));
148
+ if (args.sourcelang)
149
+ params.set("sourcelang", String(args.sourcelang));
150
+ const data = await gdeltFetch(GDELT_DOC, params);
151
+ const timelineArr = data?.timeline ?? [];
152
+ const entries = timelineArr.flatMap((t) => t?.data ?? []);
153
+ if (entries.length === 0) {
154
+ return { query, trend: "no data", timeline: [] };
155
+ }
156
+ const values = entries.map((e) => e.normvalue ?? e.value ?? 0);
157
+ const recent = values.slice(-5);
158
+ const older = values.slice(0, Math.max(1, values.length - 5));
159
+ const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
160
+ const olderAvg = older.reduce((a, b) => a + b, 0) / older.length;
161
+ const trend = olderAvg === 0 ? "emerging" :
162
+ recentAvg > olderAvg * 1.5 ? "surging" :
163
+ recentAvg > olderAvg * 1.1 ? "rising" :
164
+ recentAvg < olderAvg * 0.5 ? "fading" :
165
+ recentAvg < olderAvg * 0.9 ? "declining" : "stable";
166
+ return {
167
+ query,
168
+ trend,
169
+ recent_avg_volume: Math.round(recentAvg * 1000) / 1000,
170
+ older_avg_volume: Math.round(olderAvg * 1000) / 1000,
171
+ timeline: entries.map((e) => ({
172
+ date: e.date ?? null,
173
+ volume: Math.round((e.normvalue ?? e.value ?? 0) * 1000) / 1000,
174
+ })),
175
+ };
176
+ }
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ // GDELT News MCP. Standalone MCP server by UnClick.
3
+ // By UnClick. 180+ tools plus persistent agent memory in one install: https://unclick.world
4
+ //
5
+ // Generated from the UnClick connector by scripts/generate-standalone-mcp.mjs.
6
+ // Edit the connector in the UnClick monorepo, not here.
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
10
+ import { gdeltNewsSearch, gdeltToneAnalysis, gdeltGeoEvents, gdeltTrending, } from "./gdelt-tool.js";
11
+ const TOOLS = [
12
+ {
13
+ name: "gdelt_news_search",
14
+ description: "Search global news via the GDELT Project. Returns article titles, URLs, sources, dates, countries, and languages. No API key required.",
15
+ inputSchema: {
16
+ type: "object",
17
+ additionalProperties: false,
18
+ properties: {
19
+ query: { type: "string", description: "Search query (keywords, phrases, or operators)" },
20
+ maxrecords: { type: "number", description: "Max articles to return (default 25, max 250)" },
21
+ startdatetime: { type: "string", description: "Start datetime YYYYMMDDHHMMSS (UTC)" },
22
+ enddatetime: { type: "string", description: "End datetime YYYYMMDDHHMMSS (UTC)" },
23
+ sourcelang: { type: "string", description: "Filter by source language (e.g. 'english', 'spanish')" },
24
+ sourcecountry: { type: "string", description: "Filter by source country code (e.g. 'US', 'GB', 'AU')" },
25
+ },
26
+ required: ["query"],
27
+ },
28
+ },
29
+ {
30
+ name: "gdelt_tone_analysis",
31
+ description: "Analyse the sentiment and tone of global news coverage for a topic over time. Returns average tone scores (negative = negative coverage, positive = positive), trend summary, and timeline. Great for brand monitoring or tracking public sentiment.",
32
+ inputSchema: {
33
+ type: "object",
34
+ additionalProperties: false,
35
+ properties: {
36
+ query: { type: "string", description: "Topic or keyword to analyse" },
37
+ timespan: { type: "string", description: "Time window (e.g. '24h', '7d', '1month')" },
38
+ sourcelang: { type: "string", description: "Filter by source language" },
39
+ sourcecountry: { type: "string", description: "Filter by source country code" },
40
+ },
41
+ required: ["query"],
42
+ },
43
+ },
44
+ {
45
+ name: "gdelt_geo_events",
46
+ description: "Get geographic distribution of news events for a topic from the GDELT GEO API. Returns event clusters with location, article count, and tone score.",
47
+ inputSchema: {
48
+ type: "object",
49
+ additionalProperties: false,
50
+ properties: {
51
+ query: { type: "string", description: "Topic or keyword to map" },
52
+ maxpoints: { type: "number", description: "Max location clusters to return (default 50, max 250)" },
53
+ timespan: { type: "string", description: "Time window (e.g. '24h', '7d')" },
54
+ },
55
+ required: ["query"],
56
+ },
57
+ },
58
+ {
59
+ name: "gdelt_trending",
60
+ description: "Check whether a topic is trending in global news using GDELT article volume timelines. Returns a trend classification (surging, rising, stable, declining, fading) and volume data over time.",
61
+ inputSchema: {
62
+ type: "object",
63
+ additionalProperties: false,
64
+ properties: {
65
+ query: { type: "string", description: "Topic or keyword to check" },
66
+ timespan: { type: "string", description: "Time window (e.g. '24h', '7d', '1month')" },
67
+ sourcelang: { type: "string", description: "Filter by source language" },
68
+ },
69
+ required: ["query"],
70
+ },
71
+ }
72
+ ];
73
+ const HANDLERS = {
74
+ gdelt_news_search: (args) => gdeltNewsSearch(args),
75
+ gdelt_tone_analysis: (args) => gdeltToneAnalysis(args),
76
+ gdelt_geo_events: (args) => gdeltGeoEvents(args),
77
+ gdelt_trending: (args) => gdeltTrending(args),
78
+ };
79
+ const server = new Server({ name: "io.github.malamutemayhem/gdelt", version: "0.1.0" }, { capabilities: { tools: {} } });
80
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
81
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
82
+ const handler = HANDLERS[req.params.name];
83
+ if (!handler) {
84
+ return { content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }], isError: true };
85
+ }
86
+ try {
87
+ const result = await handler((req.params.arguments ?? {}));
88
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
89
+ }
90
+ catch (err) {
91
+ const message = err instanceof Error ? err.message : String(err);
92
+ return { content: [{ type: "text", text: message }], isError: true };
93
+ }
94
+ });
95
+ async function main() {
96
+ const transport = new StdioServerTransport();
97
+ await server.connect(transport);
98
+ }
99
+ main().catch((err) => {
100
+ process.stderr.write(`[gdelt-mcp] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
101
+ process.exit(1);
102
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@unclick/gdelt-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.malamutemayhem/gdelt",
5
+ "description": "Global news search, events, tone, themes, and geographic signals from GDELT. By UnClick (https://unclick.world).",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "unclick",
10
+ "gdelt",
11
+ "news",
12
+ "events"
13
+ ],
14
+ "author": "UnClick (https://unclick.world)",
15
+ "type": "module",
16
+ "bin": {
17
+ "gdelt-mcp": "./dist/index.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "server.json"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/index.js",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.15.1"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.6.0",
35
+ "@types/node": "^22.0.0"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/malamutemayhem/unclick.git",
41
+ "directory": "packages/standalone/gdelt-mcp"
42
+ },
43
+ "homepage": "https://unclick.world"
44
+ }
package/server.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.malamutemayhem/gdelt",
4
+ "title": "GDELT News MCP by UnClick",
5
+ "description": "Global news search, events, tone, themes, and geographic signals from GDELT. By UnClick.",
6
+ "version": "0.1.0",
7
+ "websiteUrl": "https://unclick.world",
8
+ "icons": [
9
+ {
10
+ "src": "https://unclick.world/favicon.png",
11
+ "mimeType": "image/png",
12
+ "sizes": [
13
+ "512x512"
14
+ ]
15
+ }
16
+ ],
17
+ "repository": {
18
+ "url": "https://github.com/malamutemayhem/unclick.git",
19
+ "source": "github",
20
+ "subfolder": "packages/standalone/gdelt-mcp"
21
+ },
22
+ "packages": [
23
+ {
24
+ "registryType": "npm",
25
+ "identifier": "@unclick/gdelt-mcp",
26
+ "version": "0.1.0",
27
+ "runtimeHint": "npx",
28
+ "transport": {
29
+ "type": "stdio"
30
+ }
31
+ }
32
+ ]
33
+ }