skills/dendrite/SKILL.md
Symbiont e8d7611e2b Add dendrite skill to canonical repo
Headless Chromium browser service for web browsing, scraping, and automation.
Part of the Muse ecosystem: Symbiont orchestrates, Dendrite perceives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 20:12:41 +00:00

13 KiB

name description
dendrite Headless Chromium browser on cortex.hydrascale.net — full JS execution, sessions, screenshots, Readability content extraction. Use for ANY web browsing: fetching pages, research, scraping, screenshots, login flows, form filling, SPA rendering. Prefer over Claude in Chrome (faster, runs on cortex). Trigger on: URLs in messages, "browse", "fetch", "scrape", "screenshot", "read this page", "check this link", "log in to", "go to", web research, or any task needing web access. Also use when WebFetch fails on JS-heavy pages.

Dendrite: Sensory Extension for Muse

What It Is

Dendrite is the nervous system's sensory arm — it reaches out into the web, perceives content through full Chromium rendering, and carries structured information back to the system. Named for the branching neural extensions that receive signals from the outside world.

It runs as a Docker container on cortex.hydrascale.net, exposes a REST API behind Caddy with auto-HTTPS, and includes an MCP server for native Claude integration. Full JavaScript execution, persistent sessions, Mozilla Readability content extraction, ad blocking, and minimal stealth patches.

Part of the Muse ecosystem: Symbiont orchestrates, Dendrite perceives.


Quick Reference

Item Value
Base URL https://browser.hydrascale.net
Internal URL http://localhost:3000 (from cortex via SSH)
API Key 8dc5e8f7a02745ee8db90c94b2481fd9e1deeea1e2ce74420f54047859ea7edf
Auth header X-API-Key: <key> (required on all endpoints except /health)
Health check GET /health (no auth)
Source /opt/muse-browser/ on cortex
Git repo /data/repos/muse-browser.git (bare, auto-backed up to rsync.net)
Docker container muse-browser (restart: unless-stopped)
Caddy domain browser.hydrascale.net (auto-HTTPS)

Python Helper (paste into every session that needs web access)

import urllib.request, json, urllib.error

DENDRITE_URL = 'https://browser.hydrascale.net'
DENDRITE_KEY = '8dc5e8f7a02745ee8db90c94b2481fd9e1deeea1e2ce74420f54047859ea7edf'

def dendrite(path, body=None, method=None):
    """Call the Dendrite API. Returns parsed JSON."""
    url = f'{DENDRITE_URL}{path}'
    if body is not None:
        data = json.dumps(body).encode()
        req = urllib.request.Request(url, data=data, method=method or 'POST')
        req.add_header('Content-Type', 'application/json')
    else:
        req = urllib.request.Request(url, method=method or 'GET')
    req.add_header('X-API-Key', DENDRITE_KEY)
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        err = json.loads(e.read())
        raise RuntimeError(f"Dendrite error {e.code}: {err.get('error', e.reason)}")

def dendrite_screenshot(body):
    """Take a screenshot. Returns raw PNG bytes."""
    url = f'{DENDRITE_URL}/screenshot'
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Content-Type', 'application/json')
    req.add_header('X-API-Key', DENDRITE_KEY)
    with urllib.request.urlopen(req, timeout=60) as resp:
        return resp.read()

Endpoints

POST /fetch — The workhorse

Fetch a URL and return its content as markdown, HTML, or text. Runs full Chromium with JavaScript execution. Readability extracts the main article content by default.

result = dendrite('/fetch', {
    'url': 'https://example.com/article',
    'format': 'markdown',           # 'markdown' | 'html' | 'text'  (default: markdown)
    'extractMain': True,            # Readability strips nav/ads (default: True)
    'waitFor': 'domcontentloaded',  # 'networkidle' for SPAs (default: domcontentloaded)
    'blockAds': True,               # Block trackers (default: True)
    'timeout': 30000,               # ms (default: 30000)
})
# Returns: { url, title, content, format }
print(result['title'])
print(result['content'])

When to use waitFor: 'networkidle': React/Vue/Angular SPAs, dashboards, or pages where content loads after the initial HTML. Slower (~5-10s) but catches dynamically rendered content.

When to use extractMain: false: You need the full HTML (link scraping, structured data, or when Readability strips too much on non-article pages like listings or search results).

POST /screenshot

png_bytes = dendrite_screenshot({
    'url': 'https://example.com',
    'fullPage': True,               # default: True
    'format': 'png',                # 'png' | 'jpeg'
    'waitFor': 'networkidle',       # default: networkidle (screenshots need rendering)
    'selector': '.chart',           # optional: screenshot just this element
})
with open('screenshot.png', 'wb') as f:
    f.write(png_bytes)

POST /execute

Run JavaScript in a page context. Scripts are wrapped in an IIFE — use return for values.

result = dendrite('/execute', {
    'url': 'https://example.com',
    'script': 'return document.querySelectorAll("a").length',
})
print(result['result'])  # e.g., 42

POST /interact

Interact with elements in a session. See Sessions section below.

dendrite('/interact', {
    'sessionId': sid,
    'action': 'click',      # 'click' | 'type' | 'select' | 'wait' | 'scroll'
    'selector': '#submit',
    'timeout': 5000,
})
# Returns: { ok, title, url }

POST /session / DELETE /session/:id / GET /sessions

Session lifecycle management. See Sessions section.

GET /health

No auth required. Returns: { status, sessions, activePages, uptime, timestamp }


Sessions (multi-step interactions)

Sessions maintain cookies, localStorage, and auth state across requests. Use them for login flows, multi-page navigation, form filling, and any workflow requiring state.

Sessions auto-expire after 30 minutes of inactivity. Always close when done.

Full session workflow example

# 1. Create
session = dendrite('/session', {
    'locale': 'en-US',
    'timezone': 'America/Chicago',
    'blockAds': True,
})
sid = session['id']

# 2. Navigate to login
page = dendrite('/fetch', {'sessionId': sid, 'url': 'https://app.example.com/login'})

# 3. Type credentials
dendrite('/interact', {
    'sessionId': sid, 'action': 'type',
    'selector': '#email', 'value': 'user@example.com',
})
dendrite('/interact', {
    'sessionId': sid, 'action': 'type',
    'selector': '#password', 'value': 'secret', 'submit': True,
})

# 4. Check where we landed
page = dendrite('/fetch', {'sessionId': sid})  # no url = get current page
print(page['title'], page['url'])

# 5. Click around
dendrite('/interact', {'sessionId': sid, 'action': 'click', 'selector': 'nav a.dashboard'})
page = dendrite('/fetch', {'sessionId': sid})

# 6. Always close
dendrite(f'/session/{sid}', method='DELETE')

Interact actions reference

Action Required Optional Description
click selector timeout Click element, wait for domcontentloaded
type selector, value submit, timeout Fill input. submit: true presses Enter
select selector, value timeout Select dropdown option by value
wait selector timeout Wait for element to appear in DOM
scroll selector, timeout Scroll element into view, or page bottom if no selector

Decision Guide

I need to... Use
Read an article / docs page POST /fetch (default settings)
Fetch a React/Vue SPA POST /fetch with waitFor: 'networkidle'
Scrape links or structured data POST /fetch with extractMain: false, format: 'html' then parse
Visually verify a page POST /screenshot
Extract data via JS POST /execute
Log in, fill forms, multi-step Create session → interact → close
Quick check what's at a URL POST /fetch — one line

Dendrite vs WebFetch vs Claude in Chrome

Feature Dendrite WebFetch Claude in Chrome
JavaScript execution Full Chromium None Full Chrome
Speed Fast (server-side) Fastest (no browser) Slow (screen recording)
SPAs (React, etc.) Works Fails Works
Sessions/auth flows Yes No Yes (manual)
Screenshots Yes (API) No Yes (visual)
Runs on cortex (16GB) Cowork VM Michael's MacBook
Best for Research, scraping, automation Simple static pages Visual tasks, debugging

Rule of thumb: Try Dendrite first. Fall back to WebFetch for dead-simple pages where you don't need JS. Use Claude in Chrome only when you truly need to see and interact with the visual layout (drag-and-drop, complex visual UIs).


Error Handling

try:
    result = dendrite('/fetch', {'url': 'https://example.com'})
except RuntimeError as e:
    print(f"Error: {e}")
    # Common errors:
    # 401 — Bad API key
    # 404 — Session not found (expired after 30min idle)
    # 429 — Too many concurrent pages (max 10), retry shortly
    # 500 — Navigation timeout, page error, or unreachable site

If a page times out: Try with waitFor: 'domcontentloaded' (faster, may miss lazy content) or increase timeout beyond the default 30s.

If content is empty/short: The page may be JavaScript-rendered. Use waitFor: 'networkidle'. If Readability returns too little, try extractMain: false and extract what you need manually.


Architecture

Internet                     cortex.hydrascale.net
   │                              │
   ▼                              ▼
[Caddy] ──HTTPS──▶ [Docker: muse-browser]
  :443                    :3000
                     ┌─────────┐
                     │ Fastify │ ← REST API
                     │ server  │
                     └────┬────┘
                          │
                     ┌────▼────┐
                     │Playwright│ ← Single Chromium instance
                     │ + pool   │   Multiple BrowserContexts (sessions)
                     └────┬────┘
                          │
                     ┌────▼─────┐
                     │Readability│ ← Content extraction
                     │+ Turndown │   HTML → Markdown
                     └──────────┘

Stack

  • Runtime: Node.js 20 on Debian Bookworm (Docker)
  • Browser: Playwright + Chromium (headless, with stealth patches)
  • HTTP server: Fastify v4 with CORS + API key auth
  • Content extraction: Mozilla Readability + Turndown
  • MCP: stdio transport (for Claude Desktop integration)
  • Reverse proxy: Caddy (auto-HTTPS, gzip)

Key files on cortex

Path Purpose
/opt/muse-browser/ Working directory (Docker build source)
/opt/muse-browser/src/server.js Fastify entry point
/opt/muse-browser/src/browser.js Chromium pool + sessions
/opt/muse-browser/src/extract.js Readability + Turndown
/opt/muse-browser/src/routes.js REST endpoints
/opt/muse-browser/src/mcp-stdio.js MCP server (stdio)
/opt/muse-browser/.env Secrets (API key, config)
/data/repos/muse-browser.git Bare git repo (backed up nightly)

Maintenance & Operations

Health check

health = dendrite('/health', method='GET')
print(health)  # { status: "ok", sessions, activePages, uptime, timestamp }

From cortex SSH

# Container status
docker ps | grep muse-browser
docker logs muse-browser --tail=50

# Restart
docker compose -f /opt/muse-browser/docker-compose.yml restart

# Full rebuild after code changes
cd /opt/muse-browser && docker compose down && docker compose build --no-cache && docker compose up -d

Git deploy (from Michael's Mac)

# First time
git clone root@cortex.hydrascale.net:/data/repos/muse-browser.git
cd muse-browser

# After making changes
git push origin main
# → post-receive hook auto-rebuilds container and restarts

Caddy logs (if HTTPS issues)

journalctl -u caddy --no-pager -n 30

MCP Configuration (Claude Desktop)

To use Dendrite tools natively in Claude Desktop, add to MCP config:

{
  "mcpServers": {
    "dendrite": {
      "command": "ssh",
      "args": [
        "-o", "StrictHostKeyChecking=no",
        "-i", "~/.ssh/cortex",
        "root@cortex.hydrascale.net",
        "docker exec -i muse-browser node src/mcp-stdio.js"
      ]
    }
  }
}

MCP tools available

Tool Description
fetch_page Fetch URL → markdown/html/text
take_screenshot Screenshot URL or session → PNG
run_javascript Execute JS in page context
create_session Open persistent browser session
close_session Destroy session
navigate Session: go to URL, return content
click Session: click element by selector
type_text Session: type into input field
get_page_content Session: get current page content
get_page_screenshot Session: screenshot current page

Relationship to Other Muse Components

  • Symbiont (orchestrator) can dispatch tasks that require web research → Dendrite fetches the content
  • Cortex (infrastructure) hosts and runs Dendrite as a Docker service
  • Future components can call Dendrite's REST API to perceive the web without their own browser