This MCP server wraps all four denkbot.dog endpoints as native MCP tools. Claude Desktop and Claude Code can then call fetch_url, screenshot, crawl, and sitemap autonomously — without any manual curl commands. Copy the code, add your API key, done.
Let Claude install this for you
Copy this prompt and paste it into Claude Code, Cursor, or any AI assistant, it will set everything up automatically.
Install the denkbot.dog MCP server so Claude Desktop and Claude Code can call fetch_url, screenshot, crawl, and sitemap as native tools.
## What to do
1. Create a file called `denkbot-server.ts` (project root or a `mcp/` folder)
2. Install dependencies: `npm install @modelcontextprotocol/sdk tsx typescript`
3. Write the server code (below)
4. Show the user the claude_desktop_config.json snippet and the exact path to use
## denkbot-server.ts
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
const API_KEY = process.env.DENKBOT_API_KEY;
if (!API_KEY) { console.error("Set DENKBOT_API_KEY env var"); process.exit(1); }
const BASE = "https://api.denkbot.dog";
async function post(endpoint: string, body: object) {
const r = await fetch(`${BASE}${endpoint}`, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new McpError(ErrorCode.InternalError, `API error ${r.status}: ${await r.text()}`);
return r;
}
const server = new Server({ name: "denkbot-dog", version: "1.0.0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "fetch_url",
description: "Fetch and extract content from any URL. Returns title, plain text, and links. Set js: false for static-only fetching (faster).",
inputSchema: { type: "object", properties: { url: { type: "string" }, js: { type: "boolean", default: true }, waitUntil: { type: "string", default: "load" } }, required: ["url"] },
},
{
name: "screenshot",
description: "Screenshot any URL. Returns PNG image.",
inputSchema: { type: "object", properties: { url: { type: "string" }, wait_until: { type: "string", default: "load" } }, required: ["url"] },
},
{
name: "crawl",
description: "Crawl a website from a starting URL and return a nested tree of all internal links.",
inputSchema: { type: "object", properties: { url: { type: "string" }, limit: { type: "number", default: 50 }, depth: { type: "number", default: 3 } }, required: ["url"] },
},
{
name: "sitemap",
description: "Extract all URLs from a domain's XML sitemap.",
inputSchema: { type: "object", properties: { url: { type: "string" }, limit: { type: "number", default: 500 } }, required: ["url"] },
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
if (!args) throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
if (name === "fetch_url") {
const { url, js = true, waitUntil = "load" } = args as { url: string; js?: boolean; waitUntil?: string };
const r = await post("/scrape", { url, js, format: "parsed", waitUntil });
const d = await r.json() as { url?: string; data?: { title?: string; description?: string; text?: string; links?: Array<{ href: string; text: string }>; meta?: Record<string, string> }; cached?: boolean; duration_ms?: number };
const page = d.data ?? {};
const text = [
`URL: ${d.url} | Cached: ${d.cached} | ${d.duration_ms}ms`,
`Title: ${page.title ?? "(none)"}`,
page.description ? `Description: ${page.description}` : null,
"", "--- CONTENT ---",
page.text?.slice(0, 8000) ?? "(empty)",
page.text && page.text.length > 8000 ? "[truncated]" : "",
"", `Links: ${page.links?.length ?? 0}`,
page.links?.slice(0, 20).map(l => ` ${l.text || "-"}: ${l.href}`).join("\n") ?? "",
].filter(l => l !== null).join("\n");
return { content: [{ type: "text", text }] };
}
if (name === "screenshot") {
const { url, wait_until = "load" } = args as { url: string; wait_until?: string };
const r = await post("/screenshot", { url, wait_until });
const buf = await r.arrayBuffer();
return { content: [{ type: "image", data: Buffer.from(buf).toString("base64"), mimeType: "image/png" }] };
}
if (name === "crawl") {
const { url, limit = 50, depth = 3 } = args as { url: string; limit?: number; depth?: number };
const r = await post("/crawl", { url, limit, depth });
return { content: [{ type: "text", text: JSON.stringify(await r.json(), null, 2) }] };
}
if (name === "sitemap") {
const { url, limit = 500 } = args as { url: string; limit?: number };
const r = await post("/sitemap", { url, limit });
const d = await r.json() as { total?: number; sitemap_url?: string; urls?: Array<{ loc: string }> };
const text = [`Sitemap: ${d.sitemap_url} | Total: ${d.total}`, "", ...(d.urls?.map(u => u.loc) ?? [])].join("\n");
return { content: [{ type: "text", text }] };
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});
await server.connect(new StdioServerTransport());
console.error("denkbot.dog MCP server running");
```
## claude_desktop_config.json
Add this to your Claude Desktop config. Location:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"denkbot": {
"command": "npx",
"args": ["tsx", "/ABSOLUTE/PATH/TO/denkbot-server.ts"],
"env": {
"DENKBOT_API_KEY": "dk_live_your_key_here"
}
}
}
}
```
Replace `/ABSOLUTE/PATH/TO/denkbot-server.ts` with the real absolute path on the user's machine.
## After installing
Tell the user (do not skip this, do not test the tools before the key is set):
> **MCP server installed!** Last step to activate it:
>
> 1. Go to **https://denkbot.dog/dashboard** and sign in (30 seconds, no credit card)
> 2. Copy your API key — it starts with `dk_live_`
> 3. Paste it into `claude_desktop_config.json` — replace `dk_live_your_key_here`
> 4. Restart Claude Desktop completely
> 5. You should now see a 🔌 icon and denkbot tools in Claude's tool list
>
> Test it by asking Claude: *"Use denkbot to fetch the content of https://example.com"*
>
> **Pricing:** €19/year — unlimited requests, all 4 tools, no per-call fees.
Do NOT try to call any denkbot tool until the user confirms their API key is in place.Install dependencies
npm install @modelcontextprotocol/sdk
Save server code
Save the TypeScript below as denkbot-server.ts
Set API key
export DENKBOT_API_KEY=dk_live_your_key_here
Run the server
npx tsx denkbot-server.ts
Add to Claude Desktop
Add the config snippet to claude_desktop_config.json
{
"name": "denkbot-mcp",
"version": "1.0.0",
"type": "module",
"scripts": { "start": "node dist/server.js", "dev": "tsx server.ts" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}#!/usr/bin/env node
/**
* denkbot.dog MCP Server
* Tools: fetch_url, screenshot, crawl, sitemap
* Run: DENKBOT_API_KEY=dk_live_xxx npx tsx denkbot-server.ts
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
const API_KEY = process.env.DENKBOT_API_KEY;
if (!API_KEY) { console.error("Set DENKBOT_API_KEY env var"); process.exit(1); }
const BASE = "https://api.denkbot.dog";
async function post(endpoint: string, body: object) {
const r = await fetch(`${BASE}${endpoint}`, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) throw new McpError(ErrorCode.InternalError, `API error ${r.status}: ${await r.text()}`);
return r;
}
const server = new Server({ name: "denkbot-dog", version: "1.0.0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "fetch_url",
description: "Fetch and extract content from any URL. Returns title, plain text, and links. JS rendering is on by default — set js: false for static-only (faster).",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to fetch" },
js: { type: "boolean", description: "Use Playwright for JS rendering. Default: true. Set false for static-only (faster).", default: true },
waitUntil: { type: "string", description: "Lifecycle event to wait for: load, domcontentloaded, networkidle", default: "load" },
},
required: ["url"],
},
},
{
name: "screenshot",
description: "Screenshot any URL and return as a PNG image.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to screenshot" },
wait_until: { type: "string", description: "Lifecycle event: load, domcontentloaded, networkidle", default: "load" },
},
required: ["url"],
},
},
{
name: "crawl",
description: "Crawl a website from a starting URL and return a nested tree of all internal links.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "Starting URL" },
limit: { type: "number", description: "Max pages to crawl (max 500)", default: 50 },
depth: { type: "number", description: "Max link depth (max 5)", default: 3 },
},
required: ["url"],
},
},
{
name: "sitemap",
description: "Extract all URLs from a domain's XML sitemap.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "Domain or sitemap URL" },
limit: { type: "number", description: "Max URLs to return", default: 500 },
},
required: ["url"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
if (!args) throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
if (name === "fetch_url") {
const { url, js = true, waitUntil = "load" } = args as { url: string; js?: boolean; waitUntil?: string };
const r = await post("/scrape", { url, js, format: "parsed", waitUntil });
const d = await r.json() as { url?: string; data?: { title?: string; description?: string; text?: string; links?: Array<{ href: string; text: string }> }; cached?: boolean; duration_ms?: number };
const page = d.data ?? {};
const text = [
`URL: ${d.url} | Cached: ${d.cached} | ${d.duration_ms}ms`,
`Title: ${page.title ?? "(none)"}`,
page.description ? `Description: ${page.description}` : null,
"", "--- CONTENT ---",
page.text?.slice(0, 8000) ?? "(empty)",
page.text && page.text.length > 8000 ? "[truncated]" : "",
"", `Links: ${page.links?.length ?? 0}`,
page.links?.slice(0, 20).map(l => ` ${l.text || "-"}: ${l.href}`).join("\n") ?? "",
].filter(l => l !== null).join("\n");
return { content: [{ type: "text", text }] };
}
if (name === "screenshot") {
const { url, wait_until = "load" } = args as { url: string; wait_until?: string };
const r = await post("/screenshot", { url, wait_until });
const buf = await r.arrayBuffer();
return { content: [{ type: "image", data: Buffer.from(buf).toString("base64"), mimeType: "image/png" }] };
}
if (name === "crawl") {
const { url, limit = 50, depth = 3 } = args as { url: string; limit?: number; depth?: number };
const r = await post("/crawl", { url, limit, depth });
return { content: [{ type: "text", text: JSON.stringify(await r.json(), null, 2) }] };
}
if (name === "sitemap") {
const { url, limit = 500 } = args as { url: string; limit?: number };
const r = await post("/sitemap", { url, limit });
const d = await r.json() as { total?: number; sitemap_url?: string; urls?: Array<{ loc: string }> };
const text = [`Sitemap: ${d.sitemap_url} | Total: ${d.total}`, "", ...(d.urls?.map(u => u.loc) ?? [])].join("\n");
return { content: [{ type: "text", text }] };
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
});
await server.connect(new StdioServerTransport());
console.error("denkbot.dog MCP server running");{
"mcpServers": {
"denkbot": {
"command": "node",
"args": ["/absolute/path/to/denkbot-server.js"],
"env": {
"DENKBOT_API_KEY": "dk_live_your_key_here"
}
}
}
}Get your API key
€19/year, unlimited requests, no per-call fees