Skip to main content
Glama
xenodeve

Clone Space MCP

by xenodeve

Clone Space MCP

Archive a live web page so it replays offline with the motion intact, and so an AI agent can read how the page is actually built.

Saving a page from a browser gives you a dead skeleton: the markup survives, the behaviour does not. clone-space-mcp targets the other end — a carousel that still slides, a GSAP timeline that still runs, a ScrollTrigger that still fires, with the network unplugged. Then it tells you which line makes it move.

The load-bearing decision is that replay navigates the original URL with the original document HTML, served from a HAR, so the page's real JavaScript re-executes. Serializing the hydrated DOM into a standalone file is the approach this project rejects: it breaks hydration and entry animations, which is exactly the fidelity being chased.

Quick start

bun install

# 1. archive a page
bun run mcp:call capture_page '{"url":"https://example.com/","outDir":"./out/example"}'

# 2. does it run offline?
bun run mcp:call replay_page '{"archive":"./out/example"}'
# → { "aborted": [], "unservable": 0 }        ← empty is the exit criterion

# 3. what moves, and which line defines it?
bun run mcp:call extract_behaviour '{"archive":"./out/example"}'

# 4. is the archive intact and complete?
bun run mcp:call inspect_archive '{"path":"./out/example"}'

# and, without an agent at all:
bun run inspect ./out/example -o report.html

mcp:call runs the same functions the MCP server does, with no transport and no agent — so a bug is reproducible by hand. That constraint was adopted before any MCP code was written (#8).

Connecting an agent

The server speaks MCP over stdio. Node, not Bun: three of the four tools drive Playwright (ADR 0001).

{
  "mcpServers": {
    "clone-space": {
      "command": "node",
      "args": ["scripts/mcp-server.ts"],
      "cwd": "/absolute/path/to/clone-space-mcp"
    }
  }
}

Tool

Runtime

Answers

capture_page

Node

archive this URL

replay_page

Node

does the archive run offline

extract_behaviour

Node

what moves, what drives it, which line

inspect_archive

Bun

what is in the archive, and is it complete

The order is not a suggestion. extract_behaviour runs the page in a replay, because a GSAP timeline is written down nowhere in the archive — it exists only once the page's own JavaScript has built it. Reading the HAR recovers source text that might create motion; running the page recovers the motion that did.


What it can actually do

Related MCP server: UI Perception Engine

Capture — src/capture/

An adaptive sweep, bounded so it can always stop. The sweep scrolls and waits for the page to go quiet, which is what triggers lazy content. An infinite-scroll or polling page can extend a quiet window forever, so the sweep runs against a termination budget — wall-clock, bytes, nodes, height and events (budget.ts). termination.json records the outcome and the reason, so a truncated capture is distinguishable from a complete one. It is incomplete if any request went unanswered, whatever the sweep's own reason was.

Bounded interaction, with a written refusal policy. A scroll-through only ever triggers what scrolling triggers; the effects a reader most wants explained are usually behind a click. So capture discovers candidates, builds a pure plan (interaction.ts — the rules live where bun test can reach them), and drives it (interaction-drive.ts). The driver refuses anything it judges consequential — cross-origin, downloads, new browsing contexts, file pickers, navigation, form submission, and controls whose text reads as authentication or destruction, in English and Thai — and records the rule and the fact that tripped it, so a refusal is evidence rather than a gap.

The observation layer — what the page does, not what it shipped (instrument.ts). Hooks installed before any page script runs, at the browser API layer rather than at a library. That last choice is what makes it hold: on www.chaingpt.org, THREE is not a global — it loads as an ES module — and every shader was still captured, because everything must pass through WebGL eventually.

Measured on https://www.chaingpt.org/, replayed offline: 82,613 characters of GLSL, 9 canvas contexts, 1,510 addEventListener registrations.

A WebGL shader is the sharpest case: the GLSL is assembled at runtime from strings and uniforms, so it exists in no archived file and no reader of files can ever produce it. The call stack is in the schema from version 1 on purpose — adding it later would leave every earlier observation unresolvable, and re-capturing a site is not free.

An interaction transcript (transcript.ts) — bounded, sequence-numbered, and each scroll names its container: the window, or a nested or horizontal scroller. Page coordinates alone cannot say which thing moved.

A target inventory (targets.ts) — which OOPIFs, popups, workers and worklets existed, when they attached and detached, and how they relate.

An explicit request-normalization policy (request-normalization.ts, ADR 0007) — the volatile query keys replay is allowed to treat as identical. It is a named list, not a guess: an ambiguous normalization is refused rather than resolved.

Transactional publication. Everything above is staged, validated, and only then published; commit.json holds a SHA-256 of every other file and is written last. It exists only if publication succeeded, and inspect_archive re-checks every hash against the bytes on disk.

Replay — src/replay/

The original URL, the original JavaScript. routeFromHAR(har, { notFound: 'abort' }) serves the archive and aborts anything it cannot serve, so a replay that quietly reaches the live network is impossible. A request the archive has no usable response for is reported as unservable — a fact about the archive, not about this replay.

restoreTiming holds each response until its recorded offset from the start of the page load — not its own duration, which is what made an earlier attempt time out. A page that measures itself without ordering that measurement against its own resources can otherwise settle differently offline (#187). Off by default: it costs wall-clock, measured at 825 ms → 4,577 ms on a 146-entry site.

instrument installs the observation layer on the replay, which is how extract_behaviour recovers a runtime-assembled shader from an archive.

Extract — src/extract/

A behaviour graph, not a list of CSS rules (behaviour.ts). Each node carries its mechanism, target, name, timing (duration, delay, iterations), easing, library, and — for a ScrollTrigger — the trigger detail. It also publishes unrepresented: what it saw and could not represent. A graph that silently omitted those would read as a complete description of a page it did not fully explain.

Which line, resolved through the archive's own sourcemaps (sourcemap.ts, archive-sources.ts). Every position the runtime reports is in minified coordinates — three.module.min.js:12:326662 names line 12 of a file with about fifteen lines. The extractor follows each script's sourceMappingURL to the response the capture already fetched, and turns that into a real file, a real line, and the text written on it:

origin    cdn.jsdelivr.net/npm/three@v0.151.3/build/three.module.min.js:12:326662
original  /npm/three@0.151.2/build/three.module.js:18723:5

  18723 | 	gl.shaderSource( shader, string );

Nothing is fetched here. A map the capture did not take cannot be obtained, and the report says so rather than reaching for the network.

Serve — src/serve/

Four tools over stdio, a CLI harness that reaches the same functions with no transport, and a visual inspector that needs no agent at all (inspector.ts). That last one exists because of a specific argument: if the only way to see what an archive contains is to ask an agent, then every question about whether a capture is any good needs an agent in the loop.

The layering is a rule, not a habit: src/serve/mcp.ts registers and maps errors and holds nothing else, src/serve/tools/ are plain functions a bun test can call, and src/archive/read.ts beneath them resolves and parses what capture published — owning no MCP, no transport and no interpretation, so a tool, a test, a CLI harness and the inspector all reach the same data.

Element identity — src/identity/

Capture and replay have to agree on which element they are talking about, across two runs of a page that rebuilds its own DOM. An injected script assigns a wa: id, and a ตัวจับคู่ reconciles the two runs by fingerprint — reporting what it could not match rather than guessing.

A wa: id is a handle within one run, never a key across runs (ADR 0002). Reading it as a key is how you write a reconciler that compares strings and reports total failure on a good archive.

The equivalence gate — src/equivalence/

A clone that looks right is not a measurement. bun run equivalence <url> drives the live page and the replayed archive with the same driver in one session, collects the same digest from both, and reports:

equivalence FAIL  https://labs.chaingpt.org/

residual (1)
  layout.scrollHeight  live 8544  replay 8486
unstable (0)
baseline    live 3  replay 3
network     live 248 req / 27 origins   replay 248 req / 28 origins

coverage
  scroll              100%
  motion_settled      100%
  stable_fields       100%
  interaction         100%
  listener_execution    0%

Four exit codes, because a boolean folds together things that need opposite responses: 0 PASS · 1 FAIL, a residual nothing explains · 2 INCOMPLETE, nothing was proven equal · 3 the run never produced a verdict.

Four properties it holds on purpose:

  • Coverage is a vector, never a score. A single number averages away exactly the dimension that is weak. listener_execution 0% above means the run drove no listeners — so that green row is a claim about navigation and scrolling and nothing else.

  • unobserved never counts as equal. A field only one side produced was not compared, and a page whose click listeners are never fired must not read as agreement.

  • The live page is driven more than once, as a control. Fields that disagree with themselves are reported unstable rather than blamed on the clone — and baselinePasses publishes how much evidence that control had, because a FAIL resting on no replay passes and one resting on three agreeing passes are different claims.

  • --measure-perturbation asks whether instrumenting the page changes the page. A fourth live drive with hooks installed, compared against every plain pass. Comparing against one would blame the hooks for ordinary run-to-run noise.

The network reading is reported and never compared: performance.getEntriesByType("resource") saturates at a 250-entry buffer, and one real site reads 248. Recorded verdicts for three sites are in docs/reports/.


Before you point it at anything

Response bodies are not redacted (ADR 0009). Credentials, cookies and known-sensitive request headers are (ADR 0003) — but an archive of a page contains the page, which is the point. Do not capture an authenticated page, an internal tool, or anything behind a login into a directory you would not hand to whoever can read it.

capture_page refuses to publish an archive served from a private address — loopback, link-local, private, unique-local, unspecified, or the CGNAT-shared 100.64.0.0/10 a Tailscale peer answers from — unless you pass allowPrivateNetwork. It reads each HAR entry's serverIPAddress, so it covers a subresource the page fetched itself, not only the URL you asked for. The whole capture is discarded, not the offending entry. Capturing this repo's own fixture site needs the flag, because it runs on localhost.

Two things the interaction policy cannot catch, and no structural rule can: a type="button" whose handler calls requestSubmit(), and an href="#x" whose handler issues a DELETE. Their consequence lives entirely in JavaScript no attribute describes. Treat an unfamiliar site as production.

What an archive is

One directory, all files written 0600:

File

What it records

network.har

Every request and response, bodies attached as separate files

environment.json

The browser and page environment, split into what was requested, what was observed, and what replay must reproduce

capabilities.json

Four tri-state flags: service worker, WebSocket, closed shadow root, declared sourcemap

request-normalization.json

The explicit volatile-query-key policy replay matches against

targets.json

Which targets existed during capture — OOPIFs, popups, workers — and when

transcript.json

What the driver did to the page, and what it refused to do, with the rule that refused it

termination.json

Why the sweep stopped, the budget it stopped against, and how many requests the archive holds no response for

checkpoints.json

The associations binding all of the above to one coherent run

commit.json

A SHA-256 of every other file, written last, after validation

How it is kept honest

Mechanism

Scale

What it is

Mutation corpusbun run mutate

153 entries

Re-applies a defect that actually happened and requires a named test to go red. A guard no test can fail against is indistinguishable from one that works

Fixture sitesbun run fixture:serve

3 origins

Controlled ground truth: motion, capability flags, and a cross-origin surface. Exit criteria are checked here, never on a live site

ADRsdocs/adr/

9 records

One per hard-to-reverse decision, including the alternatives rejected

Metamorphic checkbun run metamorphic

a metric

Deliberately outside every gate. Correct code still loses matches in some cases, so reporting it as pass/fail would manufacture false confidence in both directions

Two consequences worth knowing before trusting a green run:

  • More tests written from the same design add no safety. One defect stayed live while twelve tests passed, because every one of them was written from the design that contained it.

  • SURVIVED is a finding, and MUTATION NOT APPLIED is not SURVIVED. The second means the corpus no longer matches the code and measured nothing at all.

Development

Bun is the package manager and the runtime — except for code that drives a browser, which runs under Node. Playwright's client does not complete its handshake under Bun; see ADR 0001 for the measurements and what was rejected.

Command

Runtime

What it does

bun run verify

Bun

lint → typecheck → test → browser tests → build. The ship gate

bun run mutate [id…]

Bun

re-apply the corpus; each entry must be caught by its own test

bun run metamorphic

Bun

the baseline metric above

bun run equivalence <url>

Node

capture → replay → diff one page; four exit codes

bun run inspect <archive>

Node

write a visual inspector page; --no-extract needs no browser

bun run mcp:call <tool> <json>

Node

call one tool with no transport and no agent

bun run mcp:serve

Node

the stdio MCP server an agent connects to

bun run fixture:serve

Bun

the fixture site, on three origins

bun run spike

Node

the CDP measurement harness

bun run ci:lock

Bun

is GitHub Actions still refused for billing — a command, not a memory

bun run verify:status

Bun

run verify and post the result to the head SHA as t4-verify

You want

Open

To use the tools well, and know their limits

docs/agents/using-the-tools.md

Why something is built the way it is

docs/adr/

What is open right now

docs/OPEN-WORK-LEDGER.md

What shipped, and what was refuted

DONE.md

Measured verdicts on real sites

docs/reports/

What a term means here

UBIQUITOUS_LANGUAGE.md — it wins on any naming conflict

To work on this repo as an agent

CLAUDE.md

Status

Alpha, and honest about it. All four stages run end to end; what is known-incomplete is tracked rather than restated here where it would rot — see the open issues and the ledger.

Two limits worth carrying off this page:

  • listener_execution is 0% in every recorded equivalence verdict. No slice drives listeners yet, so no green verdict this project has produced is a claim about them.

  • Two replays of one archive can lay out to different heights (#187) on a page that measures itself without ordering that measurement against its own resources. restoreTiming removes it and costs wall-clock.

License

Not yet chosen.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
2hResponse time
Release cycle
Releases (12mo)
Commit activity
Issues opened vs closed

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to perceive and interact with web interfaces by extracting a unified UI Scene Graph from live URLs, providing tools for navigation, element detection, visual analysis, and state tracking.
  • A
    license
    Not graded
    quality
    C
    maintenance
    webEmbedding is a source-first website cloning engine for AI coding agents that captures live pages with Playwright, replays network evidence, and self-verifies the result.
    70
    3
    MIT

View all related MCP servers

Related MCP Connectors

  • Web search, page extraction and structured commerce, social and business data for AI agents

  • Reliable web access for AI agents: smart HTTP, rotating proxies, and full-browser rendering.

  • Turn the web into structured, reliable, actionable enterprise data for AI Agents

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xenodeve/clone-space-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server