@voidly/mcp-server 1.0.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/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Voidly MCP Server
2
+
3
+ Model Context Protocol (MCP) server for the **Voidly Global Censorship Index**. Enables AI systems like Claude, ChatGPT, and other MCP-compatible clients to query real-time internet censorship data.
4
+
5
+ ## Features
6
+
7
+ - **Real-time censorship data** from 50+ countries
8
+ - **OONI-powered measurements** - millions of data points
9
+ - **Incident tracking** - active censorship events
10
+ - **Zero configuration** - works out of the box
11
+
12
+ ## Tools
13
+
14
+ | Tool | Description |
15
+ |------|-------------|
16
+ | `get_censorship_index` | Global overview of all monitored countries |
17
+ | `get_country_status` | Detailed status for a specific country |
18
+ | `check_domain_blocked` | Check if a domain is blocked in a country |
19
+ | `get_most_censored` | Ranked list of most censored countries |
20
+ | `get_active_incidents` | Currently active censorship incidents |
21
+
22
+ ## Installation
23
+
24
+ ### Claude Desktop
25
+
26
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "voidly": {
32
+ "command": "node",
33
+ "args": ["/path/to/voidly/mcp-server/dist/index.js"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Claude Code
40
+
41
+ Add to your `.claude/settings.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "voidly": {
47
+ "command": "node",
48
+ "args": ["/path/to/voidly/mcp-server/dist/index.js"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### NPX (Coming Soon)
55
+
56
+ ```bash
57
+ npx @voidly/mcp-server
58
+ ```
59
+
60
+ ## Usage Examples
61
+
62
+ Once configured, you can ask Claude:
63
+
64
+ - "What countries have the most internet censorship?"
65
+ - "Is the internet censored in China?"
66
+ - "What's the censorship status in Iran?"
67
+ - "Are there any active internet shutdowns?"
68
+ - "Is Twitter blocked in Russia?"
69
+
70
+ ## Data Sources
71
+
72
+ - **Primary**: OONI (Open Observatory of Network Interference)
73
+ - **Update Frequency**: Every 6 hours
74
+ - **Countries Monitored**: 50+
75
+ - **License**: CC BY 4.0
76
+
77
+ ## API Endpoints Used
78
+
79
+ - `https://censorship.voidly.ai/v1/censorship-index`
80
+ - `https://censorship.voidly.ai/v1/censorship-index/:country`
81
+ - `https://censorship.voidly.ai/v1/censorship-index/incidents`
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ # Install dependencies
87
+ npm install
88
+
89
+ # Build
90
+ npm run build
91
+
92
+ # Run in development
93
+ npm run dev
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT
99
+
100
+ ## Links
101
+
102
+ - [Voidly Global Censorship Index](https://voidly.ai/censorship-index)
103
+ - [API Documentation](https://voidly.ai/api-docs)
104
+ - [Hugging Face Dataset](https://huggingface.co/datasets/emperor-mew/global-censorship-index)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Voidly MCP Server
4
+ *
5
+ * Model Context Protocol server that exposes Voidly's Global Censorship Index
6
+ * to AI systems like Claude, ChatGPT, and other MCP-compatible clients.
7
+ *
8
+ * Tools provided:
9
+ * - get_censorship_index: Global overview of all monitored countries
10
+ * - get_country_status: Detailed censorship status for a specific country
11
+ * - check_domain_blocked: Check if a domain is blocked in a country
12
+ * - get_most_censored: Get the most censored countries
13
+ * - get_active_incidents: Get active censorship incidents
14
+ */
15
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Voidly MCP Server
4
+ *
5
+ * Model Context Protocol server that exposes Voidly's Global Censorship Index
6
+ * to AI systems like Claude, ChatGPT, and other MCP-compatible clients.
7
+ *
8
+ * Tools provided:
9
+ * - get_censorship_index: Global overview of all monitored countries
10
+ * - get_country_status: Detailed censorship status for a specific country
11
+ * - check_domain_blocked: Check if a domain is blocked in a country
12
+ * - get_most_censored: Get the most censored countries
13
+ * - get_active_incidents: Get active censorship incidents
14
+ */
15
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
18
+ // Voidly API endpoints
19
+ const VOIDLY_API = 'https://censorship.voidly.ai';
20
+ const VOIDLY_DATA_API = 'https://voidly.ai/api/data';
21
+ // Country metadata for enriching responses
22
+ const COUNTRY_NAMES = {
23
+ CN: 'China', IR: 'Iran', RU: 'Russia', VE: 'Venezuela', CU: 'Cuba',
24
+ MM: 'Myanmar', BY: 'Belarus', SA: 'Saudi Arabia', AE: 'UAE', EG: 'Egypt',
25
+ MX: 'Mexico', VN: 'Vietnam', PH: 'Philippines', IN: 'India', PK: 'Pakistan',
26
+ BD: 'Bangladesh', CO: 'Colombia', BR: 'Brazil', HT: 'Haiti', TR: 'Turkey',
27
+ TH: 'Thailand', ID: 'Indonesia', MY: 'Malaysia', KZ: 'Kazakhstan', UA: 'Ukraine',
28
+ YE: 'Yemen', IQ: 'Iraq', DZ: 'Algeria', NG: 'Nigeria', KE: 'Kenya',
29
+ GH: 'Ghana', ZA: 'South Africa', AR: 'Argentina', CL: 'Chile', PE: 'Peru',
30
+ EC: 'Ecuador', US: 'United States', GB: 'United Kingdom', DE: 'Germany',
31
+ FR: 'France', ES: 'Spain', IT: 'Italy', CA: 'Canada', AU: 'Australia',
32
+ JP: 'Japan', KR: 'South Korea', NL: 'Netherlands', CH: 'Switzerland',
33
+ NZ: 'New Zealand', HK: 'Hong Kong', TW: 'Taiwan', SG: 'Singapore',
34
+ };
35
+ // Fetch helper with error handling
36
+ async function fetchJson(url) {
37
+ const response = await fetch(url, {
38
+ headers: {
39
+ 'User-Agent': 'Voidly-MCP-Server/1.0',
40
+ 'Accept': 'application/json',
41
+ },
42
+ });
43
+ if (!response.ok) {
44
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
45
+ }
46
+ return response.json();
47
+ }
48
+ // Tool implementations
49
+ async function getCensorshipIndex() {
50
+ const data = await fetchJson(`${VOIDLY_API}/v1/censorship-index`);
51
+ const { summary, countries } = data;
52
+ // Format response for AI consumption
53
+ let result = `# Voidly Global Censorship Index\n`;
54
+ result += `Updated: ${data.timestamp}\n\n`;
55
+ result += `## Summary\n`;
56
+ result += `- Full Outage: ${summary.fullOutage} countries\n`;
57
+ result += `- Partial Outage: ${summary.partialOutage} countries\n`;
58
+ result += `- Degraded: ${summary.degraded} countries\n`;
59
+ result += `- Normal: ${summary.normal} countries\n`;
60
+ result += `- Unknown: ${summary.unknown} countries\n\n`;
61
+ // Top censored countries by anomaly rate
62
+ const withData = countries
63
+ .filter(c => c.ooni && c.ooni.measurementCount > 0)
64
+ .sort((a, b) => (b.ooni?.anomalyRate || 0) - (a.ooni?.anomalyRate || 0));
65
+ result += `## Most Censored Countries (by anomaly rate)\n`;
66
+ withData.slice(0, 10).forEach((c, i) => {
67
+ const pct = ((c.ooni?.anomalyRate || 0) * 100).toFixed(1);
68
+ result += `${i + 1}. ${c.name} (${c.country}): ${pct}% anomaly rate, ${c.ooni?.measurementCount.toLocaleString()} measurements\n`;
69
+ });
70
+ result += `\n## Data Source\n`;
71
+ result += `Source: Voidly Research Global Censorship Index\n`;
72
+ result += `Based on OONI (Open Observatory of Network Interference) measurements\n`;
73
+ result += `URL: https://voidly.ai/censorship-index\n`;
74
+ result += `License: CC BY 4.0\n`;
75
+ return result;
76
+ }
77
+ async function getCountryStatus(countryCode) {
78
+ const code = countryCode.toUpperCase();
79
+ const name = COUNTRY_NAMES[code] || code;
80
+ const data = await fetchJson(`${VOIDLY_API}/v1/censorship-index/${code}`);
81
+ let result = `# Censorship Status: ${name} (${code})\n\n`;
82
+ if (data.ooni) {
83
+ const { ooni } = data;
84
+ result += `## Current Status: ${ooni.status.toUpperCase()}\n\n`;
85
+ result += `### Metrics\n`;
86
+ result += `- Anomaly Rate: ${(ooni.anomalyRate * 100).toFixed(1)}%\n`;
87
+ result += `- Confirmed Censorship Rate: ${(ooni.confirmedRate * 100).toFixed(2)}%\n`;
88
+ result += `- Total Measurements: ${ooni.measurementCount.toLocaleString()}\n`;
89
+ result += `- Last Updated: ${ooni.lastUpdated}\n\n`;
90
+ if (ooni.affectedServices && ooni.affectedServices.length > 0) {
91
+ result += `### Affected Services\n`;
92
+ ooni.affectedServices.forEach(s => {
93
+ result += `- ${s}\n`;
94
+ });
95
+ result += '\n';
96
+ }
97
+ }
98
+ else {
99
+ result += `## Status: No recent data available\n\n`;
100
+ }
101
+ if (data.activeIncidents && data.activeIncidents.length > 0) {
102
+ result += `### Active Incidents\n`;
103
+ data.activeIncidents.forEach(i => {
104
+ result += `- [${i.severity.toUpperCase()}] ${i.title}\n`;
105
+ });
106
+ result += '\n';
107
+ }
108
+ result += `## Interpretation\n`;
109
+ if (data.ooni?.anomalyRate && data.ooni.anomalyRate > 0.5) {
110
+ result += `${name} shows significant internet censorship with over 50% of measurements detecting anomalies. `;
111
+ result += `This indicates widespread blocking of websites and services.\n`;
112
+ }
113
+ else if (data.ooni?.anomalyRate && data.ooni.anomalyRate > 0.2) {
114
+ result += `${name} shows moderate internet censorship with ${(data.ooni.anomalyRate * 100).toFixed(0)}% of measurements detecting anomalies. `;
115
+ result += `Some websites and services may be blocked.\n`;
116
+ }
117
+ else if (data.ooni?.anomalyRate) {
118
+ result += `${name} shows relatively low censorship levels. Most internet services are accessible.\n`;
119
+ }
120
+ result += `\n## Source\n`;
121
+ result += `Data: Voidly Research Global Censorship Index\n`;
122
+ result += `URL: https://voidly.ai/censorship-index/${code.toLowerCase()}\n`;
123
+ return result;
124
+ }
125
+ async function checkDomainBlocked(domain, countryCode) {
126
+ const code = countryCode.toUpperCase();
127
+ const name = COUNTRY_NAMES[code] || code;
128
+ // For now, we provide general country status since domain-level data
129
+ // requires the Hydra API with authentication
130
+ const countryStatus = await getCountryStatus(code);
131
+ let result = `# Domain Block Check: ${domain} in ${name}\n\n`;
132
+ result += `## Note\n`;
133
+ result += `Domain-specific blocking data requires the Voidly Hydra API.\n`;
134
+ result += `Below is the general censorship status for ${name}.\n\n`;
135
+ result += `---\n\n`;
136
+ result += countryStatus;
137
+ return result;
138
+ }
139
+ async function getMostCensored(limit = 10) {
140
+ const data = await fetchJson(`${VOIDLY_API}/v1/censorship-index`);
141
+ const ranked = data.countries
142
+ .filter(c => c.ooni && c.ooni.measurementCount > 100)
143
+ .sort((a, b) => (b.ooni?.anomalyRate || 0) - (a.ooni?.anomalyRate || 0))
144
+ .slice(0, limit);
145
+ let result = `# Most Censored Countries (Top ${limit})\n\n`;
146
+ result += `Based on OONI measurement anomaly rates from the past 7 days.\n\n`;
147
+ ranked.forEach((c, i) => {
148
+ const pct = ((c.ooni?.anomalyRate || 0) * 100).toFixed(1);
149
+ result += `## ${i + 1}. ${c.name} (${c.country})\n`;
150
+ result += `- Anomaly Rate: ${pct}%\n`;
151
+ result += `- Measurements: ${c.ooni?.measurementCount.toLocaleString()}\n`;
152
+ if (c.ooni?.affectedServices && c.ooni.affectedServices.length) {
153
+ result += `- Affected: ${c.ooni.affectedServices.slice(0, 5).join(', ')}\n`;
154
+ }
155
+ result += '\n';
156
+ });
157
+ result += `## Source\n`;
158
+ result += `Data: Voidly Research Global Censorship Index\n`;
159
+ result += `Methodology: Based on OONI network interference measurements\n`;
160
+ result += `URL: https://voidly.ai/censorship-index\n`;
161
+ return result;
162
+ }
163
+ async function getActiveIncidents() {
164
+ const data = await fetchJson(`${VOIDLY_API}/v1/censorship-index/incidents`);
165
+ let result = `# Active Censorship Incidents\n\n`;
166
+ result += `Total: ${data.count} incidents\n\n`;
167
+ if (data.incidents.length === 0) {
168
+ result += `No active incidents currently reported.\n`;
169
+ }
170
+ else {
171
+ data.incidents.slice(0, 20).forEach(i => {
172
+ result += `## ${i.countryName}: ${i.title}\n`;
173
+ result += `- Severity: ${i.severity.toUpperCase()}\n`;
174
+ result += `- Status: ${i.status}\n`;
175
+ result += `- Started: ${i.startTime}\n`;
176
+ if (i.affectedServices.length) {
177
+ result += `- Affected Services: ${i.affectedServices.join(', ')}\n`;
178
+ }
179
+ if (i.description) {
180
+ result += `- Details: ${i.description.slice(0, 200)}${i.description.length > 200 ? '...' : ''}\n`;
181
+ }
182
+ result += '\n';
183
+ });
184
+ }
185
+ result += `## Source\n`;
186
+ result += `Data: Voidly Research Incident Tracker\n`;
187
+ result += `URL: https://voidly.ai/censorship-index\n`;
188
+ return result;
189
+ }
190
+ // Create MCP server
191
+ const server = new Server({
192
+ name: 'voidly-censorship-index',
193
+ version: '1.0.0',
194
+ }, {
195
+ capabilities: {
196
+ tools: {},
197
+ resources: {},
198
+ },
199
+ });
200
+ // Register tool handlers
201
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
202
+ tools: [
203
+ {
204
+ name: 'get_censorship_index',
205
+ description: 'Get the Voidly Global Censorship Index - a comprehensive overview of internet censorship across 50+ countries. Returns summary statistics and the most censored countries ranked by anomaly rate.',
206
+ inputSchema: {
207
+ type: 'object',
208
+ properties: {},
209
+ required: [],
210
+ },
211
+ },
212
+ {
213
+ name: 'get_country_status',
214
+ description: 'Get detailed censorship status for a specific country including anomaly rates, affected services, and active incidents.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ country_code: {
219
+ type: 'string',
220
+ description: 'ISO 3166-1 alpha-2 country code (e.g., CN for China, IR for Iran, RU for Russia)',
221
+ },
222
+ },
223
+ required: ['country_code'],
224
+ },
225
+ },
226
+ {
227
+ name: 'check_domain_blocked',
228
+ description: 'Check if a specific domain is likely blocked in a country. Returns general censorship status for the country.',
229
+ inputSchema: {
230
+ type: 'object',
231
+ properties: {
232
+ domain: {
233
+ type: 'string',
234
+ description: 'Domain to check (e.g., google.com, twitter.com)',
235
+ },
236
+ country_code: {
237
+ type: 'string',
238
+ description: 'ISO 3166-1 alpha-2 country code',
239
+ },
240
+ },
241
+ required: ['domain', 'country_code'],
242
+ },
243
+ },
244
+ {
245
+ name: 'get_most_censored',
246
+ description: 'Get a ranked list of the most censored countries by anomaly rate.',
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ limit: {
251
+ type: 'number',
252
+ description: 'Number of countries to return (default: 10, max: 50)',
253
+ },
254
+ },
255
+ required: [],
256
+ },
257
+ },
258
+ {
259
+ name: 'get_active_incidents',
260
+ description: 'Get currently active censorship incidents worldwide including internet shutdowns, social media blocks, and VPN restrictions.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {},
264
+ required: [],
265
+ },
266
+ },
267
+ ],
268
+ }));
269
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
270
+ const { name, arguments: args } = request.params;
271
+ try {
272
+ let result;
273
+ switch (name) {
274
+ case 'get_censorship_index':
275
+ result = await getCensorshipIndex();
276
+ break;
277
+ case 'get_country_status':
278
+ if (!args?.country_code) {
279
+ throw new Error('country_code is required');
280
+ }
281
+ result = await getCountryStatus(args.country_code);
282
+ break;
283
+ case 'check_domain_blocked':
284
+ if (!args?.domain || !args?.country_code) {
285
+ throw new Error('domain and country_code are required');
286
+ }
287
+ result = await checkDomainBlocked(args.domain, args.country_code);
288
+ break;
289
+ case 'get_most_censored':
290
+ const limit = Math.min(Math.max(1, args?.limit || 10), 50);
291
+ result = await getMostCensored(limit);
292
+ break;
293
+ case 'get_active_incidents':
294
+ result = await getActiveIncidents();
295
+ break;
296
+ default:
297
+ throw new Error(`Unknown tool: ${name}`);
298
+ }
299
+ return {
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ text: result,
304
+ },
305
+ ],
306
+ };
307
+ }
308
+ catch (error) {
309
+ const message = error instanceof Error ? error.message : 'Unknown error';
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: `Error: ${message}`,
315
+ },
316
+ ],
317
+ isError: true,
318
+ };
319
+ }
320
+ });
321
+ // Register resource handlers for direct data access
322
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
323
+ resources: [
324
+ {
325
+ uri: 'voidly://censorship-index',
326
+ name: 'Global Censorship Index',
327
+ description: 'Complete censorship index data in JSON format',
328
+ mimeType: 'application/json',
329
+ },
330
+ {
331
+ uri: 'voidly://methodology',
332
+ name: 'Methodology',
333
+ description: 'Data collection and scoring methodology',
334
+ mimeType: 'application/json',
335
+ },
336
+ ],
337
+ }));
338
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
339
+ const { uri } = request.params;
340
+ switch (uri) {
341
+ case 'voidly://censorship-index':
342
+ const indexData = await fetchJson(`${VOIDLY_API}/v1/censorship-index`);
343
+ return {
344
+ contents: [
345
+ {
346
+ uri,
347
+ mimeType: 'application/json',
348
+ text: JSON.stringify(indexData, null, 2),
349
+ },
350
+ ],
351
+ };
352
+ case 'voidly://methodology':
353
+ const methodData = await fetchJson(`${VOIDLY_DATA_API}/methodology`);
354
+ return {
355
+ contents: [
356
+ {
357
+ uri,
358
+ mimeType: 'application/json',
359
+ text: JSON.stringify(methodData, null, 2),
360
+ },
361
+ ],
362
+ };
363
+ default:
364
+ throw new Error(`Unknown resource: ${uri}`);
365
+ }
366
+ });
367
+ // Start server
368
+ async function main() {
369
+ const transport = new StdioServerTransport();
370
+ await server.connect(transport);
371
+ console.error('Voidly MCP Server running on stdio');
372
+ }
373
+ main().catch((error) => {
374
+ console.error('Fatal error:', error);
375
+ process.exit(1);
376
+ });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@voidly/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Voidly Global Censorship Index - enables AI systems to query real-time censorship data",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "voidly-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "censorship",
24
+ "ooni",
25
+ "voidly",
26
+ "ai",
27
+ "claude",
28
+ "chatgpt",
29
+ "llm",
30
+ "internet-freedom"
31
+ ],
32
+ "author": "Voidly Research <research@voidly.ai>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/EmperorMew/aegisvpn.git",
37
+ "directory": "mcp-server"
38
+ },
39
+ "homepage": "https://voidly.ai/censorship-index",
40
+ "bugs": {
41
+ "url": "https://github.com/EmperorMew/aegisvpn/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.0.0",
54
+ "tsx": "^4.0.0",
55
+ "typescript": "^5.0.0"
56
+ }
57
+ }