@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 +104 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +376 -0
- package/package.json +57 -0
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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|