distil-mcp 1.2.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.md +22 -0
- package/README.md +85 -0
- package/index.js +216 -0
- package/package.json +21 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 exec.io pty ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @distil.net/mcp
|
|
2
|
+
|
|
3
|
+
MCP for the [Distil](https://Distil.ai) web proxy. Provides a fall through CLI and also Gives Claude Desktop, Cursor, Windsurf and any MCP-compatible AI tool the ability to fetch web pages and search the web — clean Markdown, no noise.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `distil_scrape` | Fetch a URL and return its content as clean Markdown. Handles JavaScript-rendered pages, PDFs, and automatic content extraction. |
|
|
10
|
+
| `distil_search` | Search the web and return results as Markdown. Includes titles, URLs, and snippet text for the top results. |
|
|
11
|
+
| `distil_screenshot` | Take a screenshot of a web page and return it as an image. |
|
|
12
|
+
| `distil_render` | Render a web page (such as a single page javascript app) before trying to extract markdown. |
|
|
13
|
+
| `distil_raw` | Fetch a URL and return its raw content bypassing any attempt to render markdown. |
|
|
14
|
+
| `distil_nocache` | Fetch a URL and return its content without using the cache. |
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
### 1. Get a Distil API key
|
|
20
|
+
|
|
21
|
+
Sign up at [distil.ai](https://distil.ai) to get your API key (`dk_...`).
|
|
22
|
+
|
|
23
|
+
### 2. Add to your MCP config
|
|
24
|
+
|
|
25
|
+
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"Distil": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "@distil.net/mcp"],
|
|
33
|
+
"env": {
|
|
34
|
+
"DISTIL_API_KEY": "dk_your_key_here"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Cursor / Windsurf** — add the same block to your MCP settings file.
|
|
42
|
+
|
|
43
|
+
### 3. Restart your AI tool
|
|
44
|
+
|
|
45
|
+
The `distil_scrape`, `distil_search`, `distil_screenshot`, `distil_render`, `distil_raw` and `distil_nocache` tools will appear automatically.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
| Environment Variable | Default | Description |
|
|
50
|
+
|---------------------|---------|-------------|
|
|
51
|
+
| `DISTIL_API_KEY` | *(required)* | Your Distil API key |
|
|
52
|
+
| `DISTIL_PROXY_URL` | `https://proxy.distil.ai` | Proxy base URL (override for self-hosted) |
|
|
53
|
+
|
|
54
|
+
## Usage examples
|
|
55
|
+
|
|
56
|
+
Once installed, you can ask your AI assistant things like:
|
|
57
|
+
|
|
58
|
+
- *"Fetch https://example.com and summarise it"* → uses `distil_scrape`
|
|
59
|
+
- *"Search the web for the latest Node.js release notes"* → uses `distil_search`
|
|
60
|
+
- *"Take a screen shot of npmjs.org"* → uses `distil_screenshot`
|
|
61
|
+
- *"Go and read the web page of openai.com"* → uses `distil_render`
|
|
62
|
+
- *"Make sure you get the latest version of openclaw.ai"* → uses `distil_nocache`
|
|
63
|
+
|
|
64
|
+
## Running manually
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
DISTIL_API_KEY=dk_your_key npx @distil.net/mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The server speaks [MCP JSON-RPC 2.0](https://modelcontextprotocol.io) over stdio.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Clone the Distil repo
|
|
76
|
+
git clone https://github.com/exec-io/distil-mcp.git
|
|
77
|
+
cd distil.net/mcp
|
|
78
|
+
|
|
79
|
+
# Run tests (no dependencies needed — uses Node built-ins only)
|
|
80
|
+
node --test test.js
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT — see [LICENSE.md](../LICENSE.md) in the repo root.
|
package/index.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// distil-mcp — MCP server for the Distil proxy
|
|
3
|
+
// Exposes distil_scrape, distil_search, distil_screenshot, distil_render, distil_raw
|
|
4
|
+
// and distil_nocache as MCP tools over JSON-RPC 2.0 stdio.
|
|
5
|
+
// Usage: npx distil-mcp (set DISTIL_API_KEY env var first)
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const { URL } = require('url');
|
|
11
|
+
|
|
12
|
+
const PROXY_BASE = (process.env.DISTIL_PROXY_URL || 'https://proxy.Distil.ai').replace(/\/$/, '');
|
|
13
|
+
let API_KEY = process.env.DISTIL_API_KEY || '';
|
|
14
|
+
const VERSION = require('./package.json').version;
|
|
15
|
+
|
|
16
|
+
// ── CLI mode ──────────────────────────────────────────────────────────────────
|
|
17
|
+
// If invoked with a subcommand (fetch/search), run as one-shot CLI and exit.
|
|
18
|
+
const [,, cmd, ...cliArgs] = process.argv;
|
|
19
|
+
|
|
20
|
+
if (cmd === 'fetch' || cmd === 'search') {
|
|
21
|
+
if (!API_KEY) {
|
|
22
|
+
// Try macOS Keychain fallback
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
try {
|
|
25
|
+
process.env.DISTIL_API_KEY = execSync(
|
|
26
|
+
'security find-generic-password -s distil-api-key -w 2>/dev/null',
|
|
27
|
+
{ encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }
|
|
28
|
+
).trim();
|
|
29
|
+
API_KEY = process.env.DISTIL_API_KEY;
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
(async () => {
|
|
34
|
+
try {
|
|
35
|
+
if (cmd === 'fetch') {
|
|
36
|
+
if (!cliArgs[0]) { process.stderr.write('Usage: distil fetch <url>\n'); process.exit(1); }
|
|
37
|
+
const result = await callTool('distil_scrape', { url: cliArgs[0] });
|
|
38
|
+
process.stdout.write(result + '\n');
|
|
39
|
+
} else {
|
|
40
|
+
if (!cliArgs.length) { process.stderr.write('Usage: distil search <query>\n'); process.exit(1); }
|
|
41
|
+
const result = await callTool('distil_search', { query: cliArgs.join(' ') });
|
|
42
|
+
process.stdout.write(result + '\n');
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
process.stderr.write('Error: ' + e.message + '\n');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
} else {
|
|
50
|
+
// No CLI subcommand — fall through to MCP stdio server below
|
|
51
|
+
startMCPServer();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── HTTP helper ───────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function get(url, headers) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
const mod = parsed.protocol === 'https:' ? https : http;
|
|
60
|
+
const req = mod.get(url, { headers }, (res) => {
|
|
61
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
62
|
+
return get(res.headers.location, headers).then(resolve, reject);
|
|
63
|
+
}
|
|
64
|
+
const chunks = [];
|
|
65
|
+
res.on('data', (c) => chunks.push(c));
|
|
66
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.setTimeout(30000, () => req.destroy(new Error('Request timed out')));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Tool definitions ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const TOOLS = [
|
|
76
|
+
{
|
|
77
|
+
name: 'distil_scrape',
|
|
78
|
+
description: 'Fetch a URL and return its content as clean Markdown. Handles JavaScript-rendered pages, PDFs, and automatic content extraction.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: { url: { type: 'string', description: 'The URL to fetch and convert to Markdown' } },
|
|
82
|
+
required: ['url'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'distil_search',
|
|
87
|
+
description: 'Search the web and return results as Markdown. Includes titles, URLs, and snippet text for the top results.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: { query: { type: 'string', description: 'The search query' } },
|
|
91
|
+
required: ['query'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'distil_screenshot',
|
|
96
|
+
description: 'Take a screenshot of a web page and return it as an image.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: { url: { type: 'string', description: 'The URL of the web page to screenshot' } },
|
|
100
|
+
required: ['url'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'distil_render',
|
|
105
|
+
description: 'Render a javascript single page application (SPA) before trying to extract markdown.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: { url: { type: 'string', description: 'The URL of the javascript SPA to render' } },
|
|
109
|
+
required: ['url'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'distil_raw',
|
|
114
|
+
description: 'Fetch a URL and return its raw content bypassing any attempt to render markdown.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: { url: { type: 'string', description: 'The URL to fetch' } },
|
|
118
|
+
required: ['url'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'distil_nocache',
|
|
123
|
+
description: 'Fetch a URL and return its content without using the cache.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: { url: { type: 'string', description: 'The URL to fetch uncached' } },
|
|
127
|
+
required: ['url'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
// ── Tool execution ────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function callTool(name, args) {
|
|
135
|
+
if (!API_KEY) throw new Error('DISTIL_API_KEY environment variable is required');
|
|
136
|
+
const headers = { 'X-Distil-Key': API_KEY };
|
|
137
|
+
|
|
138
|
+
if (name === 'distil_scrape') {
|
|
139
|
+
if (!args.url) throw new Error('url is required');
|
|
140
|
+
return get(`${PROXY_BASE}/${args.url}`, headers);
|
|
141
|
+
}
|
|
142
|
+
if (name === 'distil_search') {
|
|
143
|
+
if (!args.query) throw new Error('query is required');
|
|
144
|
+
return get(`${PROXY_BASE}/search?q=${encodeURIComponent(args.query)}`, { ...headers, 'Accept': 'text/markdown' });
|
|
145
|
+
}
|
|
146
|
+
if (name === 'distil_screenshot') {
|
|
147
|
+
if (!args.url) throw new Error('url is required');
|
|
148
|
+
return get(`${PROXY_BASE}/screenshot/${args.url}`, headers);
|
|
149
|
+
}
|
|
150
|
+
if (name === 'distil_render') {
|
|
151
|
+
if (!args.url) throw new Error('url is required');
|
|
152
|
+
return get(`${PROXY_BASE}/render/${args.url}`, headers);
|
|
153
|
+
}
|
|
154
|
+
if (name === 'distil_raw') {
|
|
155
|
+
if (!args.url) throw new Error('url is required');
|
|
156
|
+
return get(`${PROXY_BASE}/raw/${args.url}`, headers);
|
|
157
|
+
}
|
|
158
|
+
if (name === 'distil_nocache') {
|
|
159
|
+
if (!args.url) throw new Error('url is required');
|
|
160
|
+
return get(`${PROXY_BASE}/nocache/${args.url}`, headers);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── JSON-RPC transport ────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const write = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
|
168
|
+
const ok = (id, result) => write({ jsonrpc: '2.0', id, result });
|
|
169
|
+
const fail = (id, code, message) => write({ jsonrpc: '2.0', id, error: { code, message } });
|
|
170
|
+
|
|
171
|
+
async function dispatch(msg) {
|
|
172
|
+
const { id, method, params } = msg;
|
|
173
|
+
try {
|
|
174
|
+
switch (method) {
|
|
175
|
+
case 'initialize':
|
|
176
|
+
ok(id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: '@distil.net/mcp', version: VERSION } });
|
|
177
|
+
break;
|
|
178
|
+
case 'initialized':
|
|
179
|
+
case 'notifications/initialized':
|
|
180
|
+
break; // notification — no response
|
|
181
|
+
case 'ping':
|
|
182
|
+
ok(id, {});
|
|
183
|
+
break;
|
|
184
|
+
case 'tools/list':
|
|
185
|
+
ok(id, { tools: TOOLS });
|
|
186
|
+
break;
|
|
187
|
+
case 'tools/call': {
|
|
188
|
+
const text = await callTool(params.name, params.arguments || {});
|
|
189
|
+
ok(id, { content: [{ type: 'text', text }] });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
if (id != null) fail(id, -32601, `Method not found: ${method}`);
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (id != null) fail(id, -32603, err.message);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Stdin reader ──────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function startMCPServer() {
|
|
203
|
+
let buf = '';
|
|
204
|
+
process.stdin.setEncoding('utf8');
|
|
205
|
+
process.stdin.on('data', (chunk) => {
|
|
206
|
+
buf += chunk;
|
|
207
|
+
const lines = buf.split('\n');
|
|
208
|
+
buf = lines.pop();
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
if (!line.trim()) continue;
|
|
211
|
+
try { dispatch(JSON.parse(line)); }
|
|
212
|
+
catch { fail(null, -32700, 'Parse error'); }
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
process.stdin.on('end', () => process.exit(0));
|
|
216
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "distil-mcp",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "MCP server for the Distil web proxy — distil_scrape, distil_search, distil_screenshot, distil_raw, distil_render, distil_nocache for Claude Desktop, Cursor, Windsurf, and any MCP-compatible AI tool",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/exec-io/distil-mcp.git",
|
|
9
|
+
"directory": "mcp"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["mcp", "distil", "web-scraping", "search", "ai", "claude", "cursor", "openclaw"],
|
|
12
|
+
"engines": { "node": ">=18" },
|
|
13
|
+
"bin": {
|
|
14
|
+
"distil": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test.js",
|
|
18
|
+
"start": "node index.js"
|
|
19
|
+
},
|
|
20
|
+
"files": ["index.js", "README.md"]
|
|
21
|
+
}
|