Content
# <img src="admin-ui/favicon.svg" width="32" align="top"> seimi-render
> 📖 **Documentation Language:** English | [简体中文](docs/README.md)
The original version of this project is [SeimiAgent](https://github.com/zhegexiaohuozi/SeimiAgent), and SeimiRender is a modernized upgrade of SeimiAgent. A Chromium-based web rendering service. Submit a URL to render the page with real Chromium browser behavior (execute JS, wait for asynchronous content), and then return the rendered complete HTML, pdf, png, json structured search results. Support long polling and WebSocket push to get results. Can run silently in the background without disturbance. Support MCP protocol, perfectly adapted to various AI Agent tools, and expand your Harness data acquisition boundary. It can also be used as an infrastructure for AI to perform RL iteration and automatic optimization in Vibe Coding.
## Features
- **Real Browser Rendering**: Based on QtWebEngine (Chromium), support JS rendering (Ajax), SPA, dynamic content
- **HTTP API**: Remote management interface, support submit task, query status, long polling pull results (html / markdown / pdf / image)
- **WebSocket Push**: Rendering completion real-time notification, convenient for free composition of various workflows.
- **MCP Protocol Support**: Built-in MCP server (port 8090), Zcode / Codex /Claude Code / Cursor and other agents can directly access and call rendering. Arbitrary network data reading or unlimited use of search engine capabilities.
- **Built-in Chrome Plugin**: Support Cookie status and self-use Chrome for 1:1 synchronization, ensure Agent or other automation scenarios use login state quickly inherit and keep consistent.
- **Login State Persistence**: cookie encrypted landing ( `data/cookies.dat` ), service restart automatic recovery, no need to resynchronize
- **Runtime Proxy Hot Switch**: `--proxy` / `POST /proxy` can change upstream proxy at any time, no need to restart, can self-expand to introduce proxy pool, thoroughly avoid IP flow control.
- **Built-in Web Management Background**: Monitoring dashboard, rendering test台, cookie management, interface documentation, Agent configuration one-key copy
- **Search Engine Result Structured Extraction**: Baidu / Bing / Google search results去广告structured, directly return JSON
- **Runtime Monitoring Indicators**: Success rate, delay quantile (p50/p90/p99), throughput, domain distribution, suitable for operation and maintenance dashboard
- **Multiple Rendering Output**: html, markdown, pdf, png/jpg, search results json structured.
- **Headless Operation**: Default offscreen platform, no display required, will not interfere with local browser operation, local operation will not interfere with browser result acquisition.
- **Basic Anti-browser Fingerprint Identification**: Basic anti-crawling identification ability.
## Architecture

## Build and Run
### Install Dependencies
| Platform | Command |
|------|------|
| Windows | `scripts\setup-windows.bat` |
| Linux | `sh scripts\setup-linux.sh` |
| macOS | `sh scripts\setup-macos.sh` |
### Build and Package
| Platform | Command |
|------|------|
| Windows | `scripts\package-windows.bat` |
| Linux | `sh scripts\package-linux.sh` |
| macOS | `sh scripts\package.sh` |
## Quick Start
Start the service (default headless offscreen):
### MacOs
```bash
build/seimi-render.app/Contents/MacOS/seimi-render --http-port 8088 --ws-port 8089
```
### AI Tool Usage Example
Standard universal MCP tool configuration:
```
{
"mcpServers": {
"seimi-render": {
"type": "http",
"url": "http://127.0.0.1:8090/mcp"
}
}
}
```
> Note: If started with a password, you need to add authorization information. You can go to the [Management Background](#browser-graphical-interface-can-remote-management-background) **Configuration Example** tab page to directly copy.
### AI Tool Usage Effect Reference
seimi-render accesses various AI agent clients through the MCP protocol, and the agent can directly drive the 'render web page → extract text → structured analysis' process with natural language, without the need for users to write URLs or parse HTML. The following table shows several typical usage tests (screenshots from real agent sessions), based on Seimi-render, you can freely construct any data capture and analysis workflow, such as stock public opinion, historical K-line analysis, etc. The following are simple scenarios as functional demonstrations. Complex tasks can customize Skills to use seimi-render as the basic data acquisition capability to complete your expected closed-loop data workflow.
| Scenario | Description | Screenshot (click to enlarge) |
|------|------|:----------------:|
| **Natural Language Drive Rendering** | In ZCode and other agent clients, users describe their needs in one sentence (e.g., "I want to know which hospitals in Beijing are reliable for dental implants"), and the agent automatically plans to call `mcp__seimi-render__browser_search` to synchronously wait for structured search engine results. If it is judged that more information is needed, it will use `get_web_content` to obtain the complete content of the specific webpage. | <a href="docs/images/zcode-agent-usage.jpg" target="_blank"><img src="docs/images/zcode-agent-usage.jpg" width="420" alt="ZCode Natural Language Drive Rendering Long Process"></a> |
| **Search Result Structured Output** | Call `browser_search` tool, return structured search results, convenient for Agent processing and identification. | <a href="docs/images/zcode-agent-mcp-result.png" target="_blank"><img src="docs/images/zcode-agent-mcp-result.png" width="420" alt="MCP render_url Return Markdown Text"></a> |
| **Multi-step Iteration** | Under complex demands, the agent will disassemble into multiple steps: first render the target page → read structured results → then render associated pages or call search engine result extraction (Baidu / Bing / Google SERP) → comprehensive output. The figure shows the agent calling multiple MCP tools within a single session + intermediate thinking complete chain. | <a href="docs/images/zcode-agent-usage-c2.jpg" target="_blank"><img src="docs/images/zcode-agent-usage-c2.jpg" width="420" alt="ZCode Multi-step Collaboration Rendering and Analysis"></a> |
> Access method see the above [AI Tool Usage Example](#ai-tool-usage-example) section; configuration JSON can be copied directly in the management background **Configuration Example** tab page.
### Manual Docking or Self-developed Application Docking Process
One command to render `https://www.sohu.com/` and get HTML (submit + long polling combined):
```bash
curl -X POST http://localhost:8088/render \
-H "Content-Type: application/json" \
-d '{"url":"https://www.sohu.com/","settle_ms":2500,"long_poll_ms":35000}'
```
Return (formatted, HTML omitted):
```json
{
"task_id": "a3f1...",
"url": "https://www.sohu.com/",
"state": "succeeded",
"elapsed_ms": 8550,
"html": "<!DOCTYPE html><html lang=\"zh-CN\"><head><title>搜狐</title>..."
}
```
Measured data: Sohu homepage (`https://www.sohu.com/`) rendering time **~8.5s**, return HTML **~220KB**, containing 44 `<script>`, 406 links, title is "搜狐" - that is, Chromium executes the complete DOM after JS.
## Command Line Parameters
| Parameter | Default Value | Description |
|------|--------|------|
| `--http-port <n>` | `8088` | HTTP service port |
| `--ws-port <n>` | `8089` | WebSocket service port |
| `--mcp-port <n>` | `8090` | MCP (Model Context Protocol) HTTP port, for Claude Code / Cursor and other agents to access |
| `--host <addr>` | `127.0.0.1` | Binding address; expose to the outside using `0.0.0.0` |
| `--concurrency <n>` | `3` | WebEngine rendering slot concurrency (see [Concurrency and Throughput](#concurrency-and-throughput---concurrency) for details) |
| `--http-threads <n>` | `8` | HTTP worker thread number |
| `--settle-ms <n>` | `2000` | Default JS settle delay, waiting for JS execution milliseconds after `loadFinished` (0–30000) |
| `--load-timeout-ms <n>` | `20000` | Single task total timeout (milliseconds) |
| `--windowed` | offscreen | Force native window QPA platform; default offscreen headless mode |
| `--no-sandbox` | off | Disable Chromium sandbox; WSL2 / container / root often need to enable |
| `--sandbox` | off | Force enable Chromium sandbox (override root automatic judgment) |
| `--verbose-chromium` | off | Display Chromium / web console log (default filter known noise) |
| `--password <pw>` | — | Management password. (Unsafe: `ps`/process list visible plaintext, recommend using the following two methods) |
| `--password-file <f>` | — | Read password from file first line (recommended) |
| `SEIMI_PASSWORD` environment variable | — | Read password from environment variable (recommended). Priority: `--password` > `--password-file` > `SEIMI_PASSWORD`. Not set = no password, open access |
| `--no-admin` | off (admin enabled) | Disable built-in management interface; default `GET /` provides management console |
| `--trusted-proxy <list>` | — | Comma-separated trusted reverse proxy IP/CIDR (e.g., `10.8.0.0/16,127.0.0.1`). After setting, `/api/login` rate limiting is based on the real client IP extracted from `X-Forwarded-For`; if not set, use TCP peer address (ignore XFF, anti-counterfeiting) |
| `--proxy <url>` | — | Upstream proxy for all Chromium traffic. Format `http://[user:pass@]host:port` or `socks5://[user:pass@]host:port`. Set via `QNetworkProxy::setApplicationProxy`, support runtime `POST /proxy` hot switch without restart. `type=direct` clear |
| `--no-stealth` | off (stealth enabled) | Disable browser fingerprint unification. Default enable stealth, all rendering instances disguised as the same Chrome desktop environment (UA/screen/WebGL/canvas), integrate into the crowd to bypass basic anti-crawling (e.g., Google) |
| `--no-warmup` | off (warmup enabled) | Disable Google session preheating. Default enable: process startup first loads `--warmup-url` (default https://www.google.com/) to get NID/CONSENT and other session cookies, **then** start dispatch timer to connect business requests - cold start zero cookie direct hit search is Google's highest risk window, preheating can significantly reduce the probability of the first search sorry |
| `--warmup-url <url>` | `https://www.google.com/` | Session preheating target URL (must be http/https). Only used when warmup is enabled; generally no need to change, special environment (e.g., walk proxy google.com is not available) can be replaced with a mirror that can stably return google domain cookies. Warmup has built-in failure self-adaptation: continuous 3 preheating failures (about 90min, Google unreachable) will automatically suspend 30min cycle, use 5min low-frequency probe; probe success immediately restore normal cycle. Status changes in the service log marked with `[warmup] SUSPENDED` / `[warmup] RESUMED` |
| `--help` | — | Display help information |
### Concurrency and Throughput (`--concurrency`)
`--concurrency` is the core regulator of throughput capacity: each rendering slot is an independent `QWebEnginePage`, sharing the same GUI thread event loop, provided by Chromium's internal multi-process to provide real CPU/network parallelism. **The more slots, the more tasks can be rendered at the same time, and the higher the overall throughput** - but there is a turning point.
Measured data (link pool 80 media articles — Sohu / Netease / Sina / Pengpai each 20, 16 cores / 32 threads machine, each gear lasted 2 minutes):
| `--concurrency` | Throughput (req/s) | Relative Increase | p50 (ms) | p99 (ms) | Success Rate |
|----------------:|:------------:|:--------:|:--------:|:--------:|:------:|
| 2 | 0.31 | Benchmark | 6125 | 9497 | 100% |
| 4 | 0.64 | +106% | 6012 | 8322 | 100% |
| 8 | 1.24 | +300% | 5984 | 8857 | 100% |
| 12 | 1.30 | +319% | 8697 | 12265 | 100% |
| 16 | 1.29 | +316% | 11757 | 17356 | 100% |
| 20 | 1.29 | +316% | 14460 | 18538 | 100% |
## Browser Graphical Interface Can Remote Management Background
seimi-render has a built-in browser management console (`GET /`, access root path to open), put "view running status, test rendering, manage cookie, configure Agent access, check interface documentation" all into a web interface - local or remote can be used, no need for SSH into the server to type curl. **Default enable**, `--no-admin` can disable. Support setting authentication.
Console tab introduction see the following table:
| Tab | Purpose | Screenshot (click to enlarge) |
|--------|------|:----------------:|
| **Runtime Statistics** | Monitoring dashboard: runtime, total requests / successful / failed / success rate, throughput (req/s), delay distribution (min/avg/p50/p90/p99/max), output type demand, request quantity and success rate Top-N by domain. Suitable for capacity planning, positioning high failure rate sites. | <a href="docs/images/runtime-info.png" target="_blank"><img src="docs/images/runtime-info.png" width="420" alt="Runtime Statistics"></a> |
| **Rendering Test台** (Chinese) | Core debugging tool: input URL, adjustable settle_ms / long polling, select output format (HTML/Markdown/PDF/screenshot), select screenshot encoding and markdown algorithm, **site-specific extraction** (Baidu / Bing / Google SERP). The following figure demonstrates Baidu search result extraction of 10 ad-free results. | <a href="docs/images/bing-search-render.png" target="_blank"><img src="docs/images/bing-search-render.png" width="420" alt="Rendering Test台 - Bing Search Result Extraction"></a> |
| **Rendering Test台** (English) | Same interface switch to English (top right "中文" switch back to Chinese), demonstrate Google search result structured extraction (`extract=google_serp`). | <a href="docs/images/google-search-extract-en.png" target="_blank"><img src="docs/images/google-search-extract-en.png" width="420" alt="English Interface - Google Search Result Extraction"></a> |
| **Cookie Status** | View rendering service holding login state cookie, by domain display "cookie number" (excluding value, anti-leakage), and browser plugin reconciliation. "Clear current session" / "permanent delete" respectively clear memory session and encrypted persistent storage (`data/cookies.dat`). | <a href="docs/images/cookies.png" target="_blank"><img src="docs/images/cookies.png" width="420" alt="Cookie Status"></a> |
| **Configuration Example** | One-key copy Agent access and call configuration: Claude Code / Cursor `mcpServers` JSON, curl `/render` example, WebSocket example, and current access token (enable `--password` display). | <a href="docs/images/config-show.png" target="_blank"><img src="docs/images/config-show.png" width="420" alt="Configuration Example"></a> |
| **Interface Documentation** | Interactive API documentation: left directory by "HTTP REST / MCP tool / WebSocket" grouping (16 HTTP endpoints + 4 MCP tools + WS's render/subscribe/auth), right details contain parameter table, curl/JSON example, response example and error code, all can be copied. | <a href="docs/images/api-docs.png" target="_blank"><img src="docs/images/api-docs.png" width="420" alt="Interface Documentation"></a> >
> Cookie synchronization source see the above [Browser Plugin](#browser-plugin) section. Management interface is pure static resources ([`admin-ui/`](admin-ui/),) with binary package distribution, no external service dependency. Remote access explicitly `--host 0.0.0.0` and strongly recommend configuring `--password` (management interface can modify proxy, inject cookie, high permission entry).
## Browser Plugin
seimi-render uses the Chromium kernel for rendering by default, without your login state. To render pages that can only be seen after logging in (personal backend, paid content, internal systems, login-required search results, etc.), you can synchronize the login cookies from your browser. A **Chrome plugin** ([`chrome-extension/`](chrome-extension/)) is provided to complete this task with one click.

The plugin reads **all** cookies from the browser, aggregates them by domain, and allows you to select and POST them to the `/cookies` interface of seimi-render with one click. After that, Chromium will automatically bring the login state when rendering pages of these domains.
### Installation (Developer Mode Loading)
1. Start the seimi-render service (default `http://localhost:8088`).
2. Open Chrome, go to `chrome://extensions`, and enable **Developer mode** in the top right corner.
3. Click **Load unpacked** and select the [`chrome-extension/`](chrome-extension/) directory.
4. The seimi-render icon appears in the toolbar, and you can use it.
> Supports Chrome / Edge / Brave and other Chromium-based browsers (MV3). Firefox's API is different and needs to be adapted.
### Operation Demonstration
1. **Click the plugin icon in the toolbar** → The popup window automatically reads all cookies from the browser, aggregates them by domain, and displays them.
- Confirm the seimi-render endpoint (default `http://localhost:8088`, will be remembered); if `--password` is set, enter the corresponding token in "Access token".
- The top right corner badge shows the connection status (green "Connected" / "Not connected" / "Detecting"); confirm that the plugin can connect to the service.
2. **Filter the domains to synchronize**:
- "Select all" at the top to select/deselect all with one click.
- Enter keywords in the search box to filter (can locate in seconds even with hundreds of domains).
- The list is sorted in descending order by the number of cookies; domains with heavy login status are ranked first (e.g., `jd.com 23`, `aliyun.com 22`).
- All are selected by default; it is recommended to **only select the needed domains** (do not fill in sensitive sessions like banks/email).
3. **Click the blue "Sync (N)" button** → The plugin batches the selected domain cookies to the `/cookies` interface, and after completion, it automatically checks and displays "N cookies synchronized (M on the server)"; if N/M is consistent, it is successful.
4. After that, seimi-render will automatically bring the login state when rendering pages of these domains.
> The "Clear server" button = one click to call `DELETE /cookies` to clear the synchronized cookies on seimi-render (used when changing accounts/debugging).
### View Synchronized Cookies on the Server
After synchronization, you can view the cookie list held by the rendering service in the **Cookie Status** page of the seimi-render management interface (`GET /`):

- The table displays the number of cookies for each domain; check with the plugin.
- The top right corner "Clear current session" / "Permanently delete" can clear the session cookies in memory and persistent storage, respectively.
- The page prompts that **cookies are encrypted and persisted to `data/cookies.dat`**, and the login state is automatically restored after restarting.
> **Privacy**: The plugin only reads cookie plaintext and does not store other information; the `GET /cookies` interface only returns the domain and count, not the cookie value, to prevent session leakage; cookies are encrypted and stored, and the key is derived from the compilation period pepper and machine binding salt (`data/seimi.key`) PBKDF2.
## HTTP API
| Method | Path | Description |
|------|------|------|
| GET | `/` | Management interface |
| POST | `/render` | Submit rendering task (can bring `long_poll_ms` for synchronous results) |
| GET | `/status/:id` | Query single task status (non-blocking) |
| GET | `/status` | Runtime panorama: cumulative count, success rate, delay distribution, throughput, domain distribution, queue snapshot |
| GET | `/result/:id?timeout=N` | Long polling pull HTML (default 25s) |
| POST | `/cookies` | Synchronize browser login state cookies (for plugin) |
| GET | `/cookies` | Synchronized cookie overview (domain → count, no value) |
| DELETE | `/cookies` | Clear synchronized cookies |
| GET | `/stats` | Queue statistics (simple snapshot, backward compatible) |
### Response Fields
| Field | Description |
|------|------|
| `task_id` | Task ID (16-bit hexadecimal) |
| `url` | Submitted URL |
| `state` | `pending` / `running` / `succeeded` / `failed` |
| `html` | Rendered complete HTML (only when `succeeded`) |
| `error` | Failure reason (only when `failed`). **Anti-crawling interception** failures start with `blocked:`; typical form: `blocked: google /sorry/ page (2 retries exhausted)` (hit Google verification page → automatic retreat retry → retry exhausted judged blocked) |
| `blocked` | Boolean, only when `state=failed` and failure is anti-crawling interception is `true` (typical scenario: Google `/sorry/` verification page retry exhausted). Cooperate with above `error` prefix `blocked:`, caller can explicitly distinguish "anti-crawling interception" and "timeout/network error" two failure paths |
| `elapsed_ms` | Time consumed from rendering start to current (ms) |
### Submit Task (Asynchronous)
Submit and return `pending` immediately, and then poll or use WebSocket for the result:
```bash
curl -X POST http://localhost:8088/render \
-H "Content-Type: application/json" \
-d '{"url":"https://www.sohu.com/","settle_ms":2500}'
# => {"task_id":"a3f1...","url":"https://www.sohu.com/","state":"pending"}
```
With long polling (submit and block until completion or timeout, get result in one step):
```bash
curl -X POST http://localhost:8088/render \
-H "Content-Type: application/json" \
-d '{"url":"https://www.sohu.com/","settle_ms":2500,"long_poll_ms":35000}'
# succeeded => {"task_id":"...","state":"succeeded","html":"...","elapsed_ms":8550}
# timed out => {"task_id":"...","state":"running","elapsed_ms":35000}
```
Request parameters:
- `url` (required): http/https address
- `settle_ms` (optional, default 2000): wait for JS execution milliseconds after `loadFinished` (0–30000); for pages with much content like Sohu, suggest 2500+
- `long_poll_ms` (optional, default 0): long polling wait for result milliseconds (>0 block until completion or timeout, upper limit 60000)
### Query Status
```bash
curl http://localhost:8088/status/a3f1...
# => {"task_id":"a3f1...","url":"https://www.sohu.com/","state":"running","elapsed_ms":1203}
```
### Long Polling Pull HTML
Suitable for scenarios like "asynchronous submission, then get result later":
```bash
curl "http://localhost:8088/result/a3f1...?timeout=30000"
# completed: {"task_id":"...","state":"succeeded","html":"<!DOCTYPE html>...","elapsed_ms":8550}
# timed out: {"task_id":"...","state":"running","elapsed_ms":30000}
# failed: {"task_id":"...","state":"failed","error":"...","elapsed_ms":...}
# anti-crawling interception failure (Google /sorry/ retry exhausted):
# {"task_id":"...","state":"failed","error":"blocked: google /sorry/ page (2 retries exhausted)","blocked":true,"elapsed_ms":...}
```
> **Anti-crawling failure type**: When rendering Google and other sites hits verification pages (typical for `/sorry/`), seimi-render will automatically retreat and retry (new page, 2 times), and retry still hits then judge as `blocked` failure – response `error` prefix `blocked:` + `blocked:true` double mark, caller can explicitly distinguish "anti-crawling interception" and "timeout/network error", the former suggests changing engine/walking agent/IP pool, the latter only needs to retry. The statistical layer `/status` also accumulates `blocked_exhausted` (see below).
### Runtime Status (`GET /status`)
Global operation and maintenance view: cumulative count, success rate, rendering delay distribution (min/avg/p50/p90/p99/max), throughput, output type demand distribution, **domain request distribution**, and current queue snapshot. Suitable for monitoring dashboard, capacity planning, and locating which type of site has a high failure rate.
```bash
curl http://localhost:8088/status
# Can bring ?domains=N to control returned domain count (default 20, upper limit 200):
curl 'http://localhost:8088/status?domains=50'
```
Returned (formatted):
```json
{
"started_at_ms": 1781876333526,
"uptime_ms": 1300620,
"uptime_human": "00:21:40",
"queue": {
"total": 2, "pending": 0, "running": 1, "done": 1
},
"totals": {
"requests": 1280, "succeeded": 1244, "failed": 36, "success_rate": 0.972,
"blocked_total": 14, "blocked_recovered": 9, "blocked_exhausted": 5
},
"latency_ms": {
"min": 980, "avg": 5230.4, "p50": 4120, "p90": 8910, "p99": 15630, "max": 28010
},
"throughput_per_sec": 0.985,
"outputs": { "html": 320, "markdown": 940, "pdf": 20 },
"domains": {
"distinct": 87,
"top": [
{ "host": "www.google.com", "total": 210, "succeeded": 205, "failed": 5, "blocked": 14, "success_rate": 0.976 },
{ "host": "www.sohu.com", "total": 600, "succeeded": 598, "failed": 2, "blocked": 0, "success_rate": 0.997 }
]
}
}
```
> Above `totals` / `domains.top[]` with `blocked` fields are **anti-crawling interception three-state count** (orthogonal to `succeeded`/`failed`, only accumulate once when task reaches final state):
Fields explanation:
| Field | Description |
|------|------|
| `started_at_ms` / `uptime_ms` / `uptime_human` | Startup moment, runtime (ms and human-readable) |
| `queue` | Current queue real-time snapshot (same as `/stats`) |
| `totals.requests/succeeded/failed` | Cumulative count since startup |
| `totals.success_rate` | `succeeded / requests`, 0–1 |
| `totals.blocked_total` | Anti-crawling page detection event total count (including each hit during retry – one task hit sorry and retreat retry, retry hit again will be counted 2 times) |
| `totals.blocked_recovered` | Experienced interception but **finally succeeded** task count (retry after jumping out of sorry task counted here, reflecting stealth + preheating + retry comprehensive repair ability) |
| `totals.blocked_exhausted` | Retry exhausted judged `blocked` **failed** task count (corresponding to response `blocked:true`). Three relations: `blocked_recovered + blocked_exhausted` ≤ hit interception task total count; `blocked_total` ≥ `blocked_recovered + blocked_exhausted` (including multiple hits during retry) |
| `latency_ms` | **Only successful task** rendering time consumption distribution; failure not counted. p50/p90/p99 use logarithmic bucket histogram approximation (fixed memory, no sample storage) |
| `throughput_per_sec` | `requests / uptime(second)` |
| `outputs.html/markdown/pdf` | Each output type requested count (bit marking statistics) |
| `domains.distinct` | Seen different host total count |
| `domains.top[]` | Top-N domain by `total`, including respective succeeded/failed/success_rate |
| `domains.top[].blocked` | Domain cumulative interception page detection count (including each hit during retry). Useful for locating high-risk sites (like `www.google.com` `blocked` much higher than others) |
> **Resource overhead**: Indicators only update once when task reaches final state, non-thermal path; delay uses fixed 32 buckets histogram (O(1) memory, O(1) update); domain mapping sets 1000 host upper limit, exceeding limit remove cold domain, prevent malicious/crawler scenario host map infinite expansion. `GET /status` query once adds lock copy snapshot, top-N sort only for returned N items, negligible overhead.
### Parse Rendering Result Example (Python)
```python
import requests, json
# Submit + long polling in one step
r = requests.post("http://localhost:8088/render", json={
"url": "https://www.sohu.com/",
"settle_ms": 2500,
"long_poll_ms": 35000,
}).json()
if r["state"] == "succeeded":
html = r["html"]
print("HTML length:", len(html))
import re
title = re.search(r"<title>(.*?)</title>", html, re.S)
print("Title:", title.group(1).strip() if title else "(None)")
print("Link count:", html.lower().count("href"))
```
Output:
```
HTML length: 221576
Title: Sohu
Link count: 406
```
Interaction Sequence Diagrams
**Submit Rendering (Long Polling for One Step, Most Commonly Used)**: The client submits with `long_poll_ms`, the server blocks until rendering is complete (or times out) and then returns.
```mermaid
sequenceDiagram
participant C as Client
participant H as HTTP Server<br/>(httplib Thread)
participant Q as RenderQueue<br/>(Thread-Safe Hub)
participant W as RenderWorker<br/>(GUI Thread)
C->>H: POST /render {url, output, settle_ms,<br/>long_poll_ms:35000, md_algorithm?, extract?}
H->>H: Authentication + JSON Parsing + URL/SSRF Verification
H->>Q: submit(url, settle, outputs, ...) → task_id
Note over H,Q: long_poll_ms >: Long Polling Blocking
H->>Q: waitForCompletion(task_id, 35000) (Blocks Current httplib Thread)
Note over W: GUI Thread Asynchronous Rendering:<br/>load → settle → Extraction/Sampling → Write Result
W->>Q: reportSucceeded(id, result)
Q-->>H: waitForCompletion Wake-Up (Task Final State)
H->>Q: snapshot(id) (Copy Result Fields)
alt succeeded
H-->>C: 200 {state:succeeded, html?, markdown?,<br/>serp_json?, has_pdf?, has_image?, elapsed_ms}
else Not Completed After Timeout
H-->>C: 200 {state:running, elapsed_ms} (No Product)
Note over C: Use GET /result/:id to Continue Polling
Rendering Failed
H-->>C: 200 {state:failed, error:"...", elapsed_ms}
end
```
**Asynchronous Submission + Secondary Pulling (Large Files / Screenshots / PDFs): `long_poll_ms:0` returns `pending` immediately, and subsequent products are pulled as needed.
```mermaid
sequenceDiagram
C as Client
participant H as HTTP Server
participant Q as RenderQueue
participant W as RenderWorker<br/>(GUI Thread)
C->>H: POST /render {url, output:"html,pdf,screenshot",<br/>long_poll_ms:0}
H->>Q: submit(...) → task_id
H-->>C: 200 {task_id, state:pending, elapsed_ms:0}
Note over C: No Blocking, Do Other Things
Note over W: GUI Thread Asynchronous Rendering (Background Completion, Product Stored in Queue)
W->>Q: reportSucceeded(id, result)
C->>H:status/:id (Lightweight, No Product)
H->>Q: snapshot(id)
H-->>C: {state:running, elapsed_ms} / {state:succeeded}
C->>H: GET /result/:id?output=html&timeout=25000 (Long Polling for HTML)
H->>Q: waitForCompletion + snapshot
H-->>C: {state:succeeded, html:"..."}
->>H: GET /pdf/:id (Pull PDF Binary)
H-->>C: application/pdf (or 404 Not Requested / 409)
C->> GET /image/:id (Pull Screenshot Binary)
H-->>C: image/png | image/jpeg (or 404 / 409)
```
**Structured Extraction of Search Engine Results (`extract=baidu_serp/bing_serp/google_serp`)**: Same path as regular rendering, after settle, follow SERP extraction branch, respond with an additional `serp_json` field.
```mermaid
sequenceDiagram
participant C as Client
participant H as HTTP Server
participant Q as RenderQueue<br/>(Thread-Safe Hub)
participant W as RenderWorker<br/>(GUI Thread)
C->>H: POST /render {url:"baidu.com/s?wd=...",<br/>output:html, extract:"baidu_serp", long_poll_ms:50000}
H->>Q: submit(...) → task_id
Note over H,Q: long_poll_ms > 0: H Blocks Until waitForCompletion
H->>Q: waitForCompletion(task_id) (Blocking)
Note over W: GUI Thread Asynchronous Rendering:<br/>load → settle to SERP Branch<br/>(Inject Engine JS to Extract Ad-Free Results from Live DOM)
W->>Q: reportSucceeded(id, result) (Contains serp_json)
Q-->>H: waitForCompletion Wake-Up (Task Final State)
H->>Q: snapshot(id) (Copy Result)
H-->>C: 200 {state:succeeded, html:"...",<br/>serp_json:{results:[...], meta:{ads_filtered:N}}}
over C: Silentation if JS Missing/Abnormal:<br/>No Field, Only Return HTML
```
## MCP (For Claude Code / Cursor and Other Agents)
seimi-render has a built-in **MCP (Model) server** (default port `8090`, based onkr04/cpp-mcp](https://github.com/hkr04/cpp-mcp), Streamable HTTP transmission, compliant with the 2025-03-26 specification). This allows AI agents like Claude Code and Cursor to **directly use seimi-render as a rendering tool** - the agent automatically decides when to render a webpage and obtain the rendered content.
### Exposed Tools
There are 4 tools, arranged from high to low semantic level - agents prefer to use high-level tools, and the underlying `render_url` tool covers all scenarios it cannot.
| Tool | Function | Required Parameters | Optional Parameters |
|------|----------|----------|----------|
| `browser_search` | Keyword Search: Automatically construct search engine URL and render, return result list. **Prioritize when no specific URL is given and you want to "search/check"** | `query` | `engine`(google/bing/baidu/duckduckgo, default google), `settle_ms`(2500), `timeout_ms`(45000) |
| `get_web_content` | Read a single article's content: use readability to extract clean markdown (remove navigation/ads/sidebar). **Prioritize when a specific URL is given to read the full text** | `url` | `md_algorithm`(readability/conservative, default readability), `settle_ms`(2500), `timeout_ms`(45000) |
| `render_url` | Full-featured Renderer: any URL, any output combination. **Use for PDFs/screenshots/raw HTML/manual search engine URLs/custom settle** | `url` | `output`(markdown/html/pdf/screenshot comma combination, default markdown), `md_algorithm`(conservative/readability, default conservative), `format`(auto/png/jpg, only affects screenshots), `settle_ms`(2500), `timeout_ms`(45000) |
| `get_render_result` | Supplement results for submitted tasks by task_id: `render_url` returns `running` and then polls, or re-pulls completed products | `task_id` | `output`(markdown/html/screenshot/pdf, default markdown), `timeout_ms`(5000) |
**Tool Selection Quick Reference** (each tool's description also includes this, and agents will automatically judge):
- User says "search/check/Google it/help me understand…" but no URL is given → `browser_search`
- User provides an article/page URL to read the content → `get_web_content`
- Need PDF/screenshot/raw HTML/conservative markdown/manual search engine URL/custom settle → `render_url`
- Previous step returns `state=running` (slow site/anti-crawling not completed) → `get_render_result` to poll and supplement
**Output Format Description** (`render_url`'s `output` parameter):
| Value | Return Form |
|----|----------|
| `markdown` (default) | MCP text content, clean readable text |
| `html` | MCP text content, rendered complete HTML |
| `pdf` | Pull `/pdf/<id>` binary → base64 encode to text content (agent decodes and saves as .pdf) |
| `screenshot` | Pull `/image/<id>` binary → MCP native **image content** (base64 + mimeType, agent can directly display image) |
Supports comma combinations (e.g., `markdown,screenshot`): text part comes first, screenshot as image content follows, PDF as base64 text at the end.
> **`browser_search` Engine Differences**: `baidu`/`bing`/`google` use SERP structured extraction (`extract=baidu_serp` etc.), return ad-free JSON result arrays (each containing title/url/snippet/source, with "related searches"); `duckduckgo` returns whole page (may contain ads). If an anti-crawling verification page is hit, return `BLOCKED` prompt and engines/retTools internally use local127.0.0:8088`) to call the rendering API, existing thread-safe rendering MCP server **does not directly touch WebEngine**.
### Authentication (When `--password`)
When seimi enabled with a password (`` / `--password / `SEIMI_PASSWORD`), the MCP endpoint8090`) is also protected: each request must carry `Authorization: Bearer <token>`, with the token **/WS using the token** (`HttpcomputeToken` derived). The MCP server runs in an independent thread, using constant time comparison to verify tokens (preventing timing side channels).
Each agent client passes the token in the request header (specific field names vary by client documentation, most support custom headers):
```json "mcp {
"seimi {
" "http",
"url": "http://localhost:8090/mcp",
"headers": {
": "Bearer <your token>"
}
}
}
}
```
> No need configuration if no password is enabled (default MCP only binds `127.0.0.1`, not exposed publicly).
### Claude Code Connection
Add the following to Claude configuration (`~/..json` or projectcp.json`):
```json
{
"m": {
"-render": {
"type": "http",
"url": "http://localhost:8090/mcp"
}
}
}
imi-render is running (`./build/seimi-render.app/Contents/MacOS/seimi-render`). After connection, in Claude Code, you can directly say "help me render https://www.sohu.com/ and get markdown", and Claude will automatically call the `render_url` tool.
### Cursor Connection
Cursor → Settings → MCP → Add new MCP server:
```json
{
"mcpServers": {
"seimi-render": {
"url": "http://localhost:8090/mcp"
}
}
}
```
### Startup Parameters
```bash
# Default MCP port 8090
./seimi-render --mcp8090
# Custom
./seimi-render --http-port 8088 --mcp-port 9090
```
### Manual Verification of MCP Endpoint
MCP is a stateful session protocol (initialize → initialized notification → tools/list). Complete process example (Streamable HTTP):
```bash
# 1. Initialize, get session id
SID=$(curl -s -D - -X POST http://localhost:8090/mcp \
-H "Content-Type: application/json" \
-H "Accept/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
| grep -i "^mcp-session-id:" | sed 's/ //I' | tr -d '\r\n')
# 2. Send initialized notification (required, otherwise session not ready)
curl -X POST http://localhost:8090/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. List tools
curl -X POST http://localhost:8090/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 4. Call render_url
curl -X://localhost:8090/mcp \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"render_url","arguments":{"url":"https://www.sohu.com/"}}}'
```
> It is recommended to use [MCP Inspector](https://github.com/model/inspector) (`npx @modelcontextprotocol/inspector`) for visual debugging: fill in the URL `http://localhost:8090/mcp`, it automatically handles handshake/session, and you select tools to call.
### Implementation Notes
(cpp-mcp) comes with a separate cpp-httplib, which is different from the project's version. To avoid ODR conflicts versions of header-only libraries same process can crashes), ** and use the project's** - seeLists.txt` include path design in the `seimi-m target.
- MCP server runs in an independent thread (non-blocking mode, managed by cpp-m server + maintenance thread), HTTP rendering service (8088) and WebSocket (8089) work independently, and the three do not affect each other. If the MCP port fails to start, it will only issue a warning and not affect the main rendering functionality.
Session Capacity and Recycling**: `max=64` (leaving room for), `session_timeout=3600` thread recovers idle sessions older than 1 hour10s). cpp-mcp has been patched to **automatically rebuild expired sessions** - clients bringing any session ID to call tools will succeed and reuse the ID, so session configuration only affects memory accumulation, not availability. The must be started with non-blocking `start(false)` to take effect; otherwise, sessions will and not decrease new connections will 503.
- **Authentication Patch**: When a password is enabled, the upstream `set_auth_handler` of cpp-m an empty stub, which has been patched ined `mcp_server.cpp` to enforce authentication a handler signature `(token, path) ->- MCP port binding
Sequence Diagrams
MCP tools do not directly interact with WebEngine. Instead, they use the httplib client to callback the local HTTP API (`127.0.0.1:8088`) and then assemble the result into MCP content (text/image) to return to the agent.
**Connection Handshakerender_urlRender and)**:
```mermaid
sequenceDiagram
participant A as
participant M as MCP Server<br/8090, Independent Thread)
participant H as Render HTTP API<br/>(:8088)
Note over A,M: Handshake (Each request: Bearer)
>M: initialize {protocolVersion, clientInfo}
M--: 200 + Mcp-Session-Id/>{capabilities:{listChanged:false}}}
A->> notifications/initialized
M-->>A: 202
->>M: tools/call render_url<brurl, output:"markdown,screenshot"}
M->>H:render {url, output, long_poll_ms:timeout}
alt Render Timeout
H-->>M: {state:running, task_id}
M-->>A:text "still call get_render else Render Success
H-->> {state:succeeded, markdown:"...", imageimage/<id>"}
opt Contains Screenshot
M->>H: GET /image/<id>
H-->>M: <PNG/JPEG bytes>
end
opt Contains PDF
M->>H /pdf/<id>
H-->>M: <PDF bytes>
end
M-->>A: content:[<br/>{type text:"[n"+markdown},/>{type:image, data:base64, mimeType:"image/png"}]
end
```
**`browser_search` (Keyword Search +)**:
```msequenceDiagram
participant A as AI participant M as MCP Server
participant H as Render HTTP API
A->>M: tools/call browser_search {query:"Python", engine:"baidu"}
M->>M: Construct baidu.com/s?wd=Python
M->>H: POST /render {url, output: extract:"baidu_serp",/>long_poll_ms:45000}
H: Render + Inject baidu_serp.js<br/>Extract ad-free results → serp_json
-->>M: {state:succeeded, serp_json:{results:[...],<br/>meta:{ads_filtered:5}}}
alt Anti-scraping Intercept Page
M-->>A:[text "BLOCKED: Verification page, tryswitch engine"]
else Normal Results
M-->>A: content:[text "[browser_search engine=baidu count=8]\n```json\n[...]\n```\nResults:\n1. **Title** — Source\n https://..."]
end
Note over A: Agent can call get_web_content to read the full text of any result
```
**`get_web_content` (Read Single Article Body)**:
```msequenceDiagram
participant A Agent
participant MCP Server
participant Render HTTP API
>M: tools/call_content {url:"https/article"}
M->>H: POST /render {url, output:markdown,<br/>md_algorithm:"", long_poll_ms:}
Note over H: Readability content extraction>(Non-article pages automatically fallback to conservative/raw)
H-->>M: {state, markdown:"# Title\n\nBody...",<br/>md:"readability"}
>>A: content "[get_web_content url md=readability]\# Title\n\nBody..."]
```
## Cookie Synchronization ( Pages)
seimi-render uses a clean WebEngine profile, which does not have. To render pages that credentials (e.g., personal dashboard, paid content), browser's login cookies.Chrome extension** (`chrome`) is provided for synchronization.
### One- with Chrome Extension (Recommended)
```bash
# 1imi-render
./build/seimi-render --http-port 8088 --ws-port 8089
```
1. Open Chrome and navigate to `chrome://extensions` → Enable "Developer mode" → Click "Load unpacked" → Select the `chrome-extension/` directory.
2. Click the seimi-render icon in the toolbar popup will automatically cookies from the browser by domain (select all by default).
3. domains to synchronize (supports/search filtering) and click "Sync".
4. After synchronization, it will display "Syn cookies (M cookies on)".
The rendering from these domains will automatically credentials. See [`chrome.md`](chrome-extension) for details.
### HTTP API
Alternatively, use the API directly (e.g., from):
```bash
# Batch synchronize cookies (fields correspond to Chrome cookies.getAll() output)
curl -X POST http://localhost:8088/cookies \
-H "Content application/json" \
-d '{"cookies":[
":"sid","value","domain":".examplehostOnly":false,
secure":true,"httptrue,"path":"expirationDate":1893456000},
{"name":"token","value":"xyz","domain":"example.org","hostOnly}
]}'
# => {"stored":2,"true}
# Overview (domain → count, no values session security)
curl http://localhost:8088/ => {"total":domains":[{"domain":"examplecount":1},{"*.example.com","count":}
# Clear cookies
curl -X DELETE http://localhost:8088/cookies => {"cleared":true}
```
Field explanations:
| Description |
|------|| `name`value` | Required |
domain` | DomainhostOnly=true`, used as origin host, no setexact host match) `hostOnly` | exact host only (no subdomains), false = include (domain automatically prefixed dot) |
| `secure` / `httpOnly` | Directly mapped to cookie| `path` to `/` |
|Date` | Epoch seconds0 considered session cookiesespan of the process) |
> **Security**: Cookies in memory only (`NoPersistentCookies`), not persisted to restarting the service clears them. `GET /cookies` only returns domain counts, not values. Synchronization uses local HTTP (localhost by default), and cookies do not leave the local machine.
## WebSocket Push
Suitable for scenarios requiring asynchronous submission and server-initiated notifications (avoiding client-side polling). WebSocket supports two operations, be performed over a single connection:
- **`render`**: Send a rendering request (submit URL; the server responds with`, and pushes `finished` when rendering is complete.
subscribe`**: Subscribe to an existing task (usually created `render` message `/render`); pushes` when the task.
After receiving `finished`, use HTTP `/result/: to retrieve the HTML. WebSocket workflow:
```
Client → {"action":"render":"https://www.sohu.com/","settle_ms":2500}
Serverevent":"created","task_id":"..."} #Server ← {"event":"finished","task_id":"...","state":"succeeded"} Rendering complete
```
### Message Format
Connect to `localhost:8089/` (text frame, JSON encoded follows a request-response + model: clients send requests, and the server responds with confirmation (`created`/`subs then pushes `finished the task is complete.
→ Server: Two**① `render` — Submit Rendering Request** (recommended, one connection for submission retrieval)
| Field | Type | Required | Description |
------|----------|-------------|
| `action` | string | Fixed `"render"` `url` | string | HTTP/HTTPS| `settle_ms` | No | Milliseconds to wait for JS execution after (0–3002000| `output` | string \| array | No | Product type, comma-separated string (,markdown,pdf"` ) or array semantics as HTTP `/ `md` / `image` / `png` / `jpg` are aliases). `html` |
| `md_algorithm` | string | No | Markdown algorithm:servative` (default `readability` semantics as HTTP) |
` | string | No | Screenshot encoding: `auto` (default) /` / `jpgsame semantics as HTTP) |
The server submits the task queue, **automatically subscribes the connection to the task**, and responds with `created`. When rendering is complete, it pushes `finished`. A real example:
```json
{"action":"render","url":"https://www.sohu.com/","settle_ms":2500}
```
**② `subscribe` — an Existing Tasktasks are usually created `/render` or another `render` message)
| Field | Type | Required | Description |
|------|------|----------|-------------|
| `action` | string | Yes | Fixed `"subscribe"` |
| `task_id` | string | Yes | Task ID to subscribe to (16 hexadecimal digits) |
```json
{"action":"subscribe","task_id":"f1c40ecda5bd41b6"}
```
> A single connection can send multiple requests to switch or subscribe to different tasks; new subscriptions do not clear subscriptions to old tasks (a single connection can monitor multiple tasks simultaneously).
#### Server → Client: Responses and Pushes
The server sends five types of messages, distinguished by the `event` field:
| `event` | Triggering Condition | Carried Fields |
|---------|----------|----------|
| `created` | `render` request accepted and submitted to rendering queue | `task_id`, `url` |
| `subscribed` | `subscribe` request valid, subscription successful | `task_id` |
| `finished` | Subscribed task reaches final state (success or failure), **pushed actively** | `task_id`, `state` |
| `authorized` | Password enabled and authentication passed (connection URL `?token=` or `auth` action); if no password is enabled, `auth` action also returns this | — |
| `error` | Request is illegal (missing fields / non-JSON / illegal URL / unknown action) | `message` |
Real example (complete interaction of rendering Sohu homepage via WS, one connection for the entire process):
```
Client → {"action":"render","url":"https://www.sohu.com/","settle_ms":2500}
Server ← {"event":"created","task_id":"64fe7c39de9e404e","url":"https://www.sohu.com/"} # Accepted
Server ← {"event":"finished","task_id":"64fe7c39de9e404e","state":"succeeded"} # Rendering complete
# Then use HTTP GET /result/64fe7c39de9e404e to retrieve HTML
```
`finished` `state` values:
| `state` | Meaning |
|---------|---------|
| `succeeded` | Rendering successful, HTML ready, can be retrieved with `/result/:id` |
| `failed` | Rendering failed (timeout, network error, HTTP 4xx/5xx, etc.); JSON returned by `/result/:id` contains `error` field |
#### Error Responses
When requests are illegal, the server returns an `error` message. In most cases, **the connection remains**, and new requests can be sent; two types of authentication errors will close the connection (close code `1008` Policy Violation):
| Client Sends (Error) | Server Returns | Closes Connection |
|--------------------|------------|------------------|
| `hello world` (non-JSON) | `{"event":"error","message":"invalid json"}` | No |
| `{"action":"render"}` (missing `url`) | `{"event":"error","message":"missing 'url'"}` | No |
| `{"action":"render","url":"ftp://x"}` (non-HTTP/HTTPS) | `{"event":"error","message":"url must/https"}` | No |
| `{"action","url":"http://.0.}` (private/metadata) | `{"event":"":"url blockedRF guard: ..." No |
| `{"action":"subscribe"}` (missing `task_id`) | `{"event":"error":"missing 'task_id'"}` | No |
|action":"foo"unknown action) | `{"event":"error","message":"unknown action; expect 'render' or 'subscribe'"}` | No |
| Task table overloaded (flooding/ backlog) | `{"","message":"server overloaded, retry later"}` | No |
| Enabled password but sent render/subscribe without authentication | `{"event":"error","message":"unauthorized; send {\"action\":\"auth\",...} or connect with ?token="}` | Yes (1008) |
| `{"action":"auth","token":"incorrect"}` (incorrect token) | `{"event":"error","message":"invalid token"}` | Yes (1008) |
After receiving `finished`, use HTTP `/result/:id` to retrieve the HTML.
### Use Case: Rendering Sohu Homepage and Receiving Completion Push (Python, Pure Standard Library, Directly Runnable)
The entire process uses only one WS connection to complete "submit rendering request → receive completion push", and finally uses HTTP to pull the HTML:
```python
import socket, base64, os, struct, json, re
import urllib.request
HTTP, WS_PORT = "http://localhost:8088", 8089
def ws_connect(port):
s = socket.create_connection(("127.0.0.1", port), timeout=10)
key = base64.b64encode(os.urandom(16)).decode()
s.sendall(f"GET / HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\n"
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n\r\n".encode())
buf = b""
while b"\r\n\r\n" not in buf:
buf += s.recv(4096)
assert b"101" in buf.split(b"\r\n")[0]
return s
def ws_send(s, text):
payload, mask = text.encode(), os.urandom(4)
h = bytearray([0x81]); l = len(payload)
if l < 126: h.append(0x80 | l)
elif l < 65536: h.append(0x80 | 126); h += struct.pack(">H", l)
else: h.append(0x80 | 127); h += struct.pack(">Q", l)
h += mask
s.sendall(bytes(h) + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))
def ws_recv(s):
s.recv(1); ln = s.recv(1)[0] & 0x7f
if ln == 126: ln = struct.unpack(">H", s.recv(2))[0]
elif ln == 127: ln = struct.unpack(">Q", s.recv(8))[0]
buf = b""
while len(buf) < ln: buf += s.recv(ln - len(buf))
return buf.decode(errors="replace")
# 1) WS send render request (Sohu homepage)
ws = ws_connect(WS_PORT)
ws_send(ws, json.dumps({"action": "render",
"url": "https://www.sohu.com/", "settle_ms": 2500}))
created = json.loads(ws_recv(ws)) # {"event":"created","task_id":"..."}
assert created["event"] == "created", created
task_id = created["task_id"]
print(f"[Accepted] task_id={task_id}")
# 2) Wait for rendering completion push
ws.settimeout(60)
finished = json.loads(ws_recv(ws)) # {"event":"finished","state":"succeeded"}
print(f"[Push] {finished['state']}")
# 3) Use task_id to pull rendered HTML via HTTP
result = json.loads(urllib.request.urlopen(
f"{HTTP}/result/{task_id}", timeout=60).read())
html = result["html"]
title = re.search(r"<title>(.*?)</title>", html, re.S)
print(f"[Result] HTML={len(html)} bytes Title={title.group(1).strip()}")
ws.close()
```
Actual output (real data):
```
[Accepted] task_id=64fe7c39de9e404e
[Push] succeeded
[Result] HTML=221331 bytes Title=Sohu
```
### Use Case
If `pip install websockets requests` is installed:
```python
import asyncio, json, re
import websockets, requests
HTTP = "http://localhost:8088"
WS_URL = "ws://localhost:8089/"
async def main():
# One WS connection completes: render submission → created acceptance → finished push
async with websockets.connect(WS_URL) as ws:
await ws.send(json.dumps({"action": "render",
"url": "https://www.sohu.com/", "settle_ms": 2500}))
created = json.loads(await ws.recv()) # {"event":"created","task_id":"..."}
task_id = created["task_id"]
finished = json.loads(await ws.recv()) # {"event":"finished","state":"succeeded"}
# Pull rendered HTML via HTTP
html = requests.get(f"{HTTP}/result/{task_id}").json()["html"]
title = re.search(r"<title>(.*?)</title>", html, re.S)
print(f"Sohu: {title.group(1).strip()} | state={finished['state']} | HTML {len(html)} bytes")
asyncio.run(main())
```
### Use Case: Multi-Task Concurrency + Push (Production Scenario)
One connection sends multiple `render` requests continuously, and the one that finishes rendering first receives `finished` (not necessarily in the order of submission), without mixing HTTP/WS:
```python
import asyncio, json, re
import websockets, requests
HTTP = "http://localhost:8088"
WS_URL = "ws://localhost:8089/"
URLS = ["https://www.sohu.com/"] * 3 # Concurrent 3 times for the same site
async def main():
async with websockets.connect(WS_URL) as ws:
# Send multiple renders continuously
for u in URLS:
await ws.send(json.dumps({"action": "render", "url": u, "settle_ms": 2500}))
await ws.recv() # Each one receives created first
# Receive finished in the order of completion
for _ in URLS:
msg = json.loads(await ws.recv()) # {"event":"finished","task_id":...,"state":...}
r = requests.get(f"{HTTP}/result/{msg['task_id']}").json()
print(f"{msg['task_id']} -> {msg['state']} HTML {len(r.get('html',''))} bytes")
asyncio.run(main())
```
> If a task is created by HTTP `/render` (e.g., submitted by another process), use `{"action":"subscribe","task_id":"..."}` to subscribe and receive `finished` on the same connection. `render` and `subscribe` can be mixed on the same connection.
### Interactive Sequence Diagram
**WS Submit Rendering + Receive Completion Push**: WS only pushes events (`created`/`finished`), and the product (html/markdown) still needs to be pulled via HTTP `/result/:id`.
```mermaid
sequenceDiagram
participant C as Client
participant WS as WebSocket Server<br/>(GUI thread signal slot)
participant Q as RenderQueue
participant P as RenderPool<br/>(GUI thread)
C->>WS: Connect to ws://host:8089/?token=<token>
alt token valid
WS-->>C: {"event":"authorized"}
else no token
Note over C,WS: Connection kept, first message can be auth
C->>WS: {"action":"auth","token":"<token>"}
WS-->>C: {"event":"authorized"} (or error + close 1008)
end
C->>WS: {"action":"render","url":"https://...","output":"html"}
WS->>WS: Parameter parsing + SSRF check
WS->>Q: submit(url, settle, outputs, ...) → task_id
WS->>WS: Automatically subscribe to this task on this connection
WS-->>C: {"event":"created","task_id":"...","url":"..."}
Note over P: GUI thread asynchronous rendering (not blocking WS)
P->>Q: reportSucceeded(id, result)
P-->>WS: taskFinished(id) signal
WS-->>C: {"event":"finished","task_id":"...","state":"succeeded"}
Note over C: finished only contains task_id + state, no product content
C->>WS: (via HTTP) GET /result/:id?output=html
```
**Cross-Transport Subscription: HTTP Submission → WS Receive Push**: One process submits via HTTP, and another process subscribes to the same task_id via WS.
```mermaid
sequenceDiagram
participant HC as HTTP Client
participant H as HTTP Server
participant WC as WS Client
participant WS as WebSocket Server
participant Q as RenderQueue
participant P as RenderPool<br/>(GUI thread)
HC->>H: POST /render {url} (long_poll_ms:0)
H->>Q: submit → task_id
H-->>HC: {task_id, state:pending}
WC->>WS: {"action":"subscribe","task_id":"<above id>"}
WS-->>WC: {"event":"subscribed","task_id":"..."}
Note over P: GUI thread asynchronous rendering (regardless of who submitted, task is consumed here)
P->>Q: reportSucceeded(id, result)
P-->>WS: taskFinished(id) signal
WS-->>WC: {"event":"finished","task_id":"...","state":"succeeded"}
Note over WC: Then use HTTP /result/:id to retrieve product
```
## License
This project (seimi-render's own source code, located in [`src/`](./src/), [`scripts/`](./scripts/), [`chrome-extension/`](./chrome-extension/)) is open-sourced under **Apache License 2.0**, all rights reserved © 2026 [wanghaomiao.cn](mailto:et.tw@163.com). The full text of the agreement can be found in [`LICENSE`](./LICENSE), and the attribution statement can be found in [`NOTICE`](./NOTICE).
### Third-Party Dependencies
This project uses the following third-party components, whose licenses are independent and copyright notices are retained in their respective source files:
| Dependency | Purpose | License |
|------|------|------|
| [Qt 6](https://www.qt.io/) (Qt WebEngine / Network / WebSockets etc.) | Chromium rendering core, network stack | **LGPL v3** (open-source option) / commercial dual licensing |
| [cpp-mcp](./third_party/cpp-mcp/) | MCP protocol implementation | MIT |
| [cpp-httplib](./third_party/httplib.h) | HTTP service | MIT |
| [html2md](./third_party/html2md.cpp) / [table](./third_party/table.cpp) | HTML→Markdown conversion | MIT |
| [qaes](./third_party/qaes/) | AES encryption | Public Domain |
| [Mozilla Readability](./third_party/readability/) | Content extraction | Apache License 2.0 |
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available