Skip to content
Conduit

mcp server

portal

Turn any website into a walkable 3D world. Reads a page's real structure and lays out a city from it: sections become buildings, links become doors, images become billboards. Pure layout in JS, a clie

nirholas/portalcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • Node.js 20 or newer
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "nirholas-portal": {
      "command": "npx",
      "args": [
        "-y",
        "@three-ws/portal"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 25, 2026

@three-ws/portal

GitHub stars License Last commit PRs welcome AI agent friendly

Turn any website into a walkable 3D world.

Paste an address; get a place. Every section becomes a building sized by what it says, every link becomes a door you can step through, every image becomes a billboard, every code block becomes a monolith. Walk it in the browser, download it as a .glb, embed it in your own page, or hand it to an agent over MCP.

Quick start · How a page becomes a place · API · CLI · MCP · Manners


Why

Every tool that "visualizes a website" draws a node graph. A graph tells you a site has 40 pages; it does not tell you which section carries the weight, where the dead ends are, or what it feels like to arrive. A place does, because size, distance and direction are things people read without being taught.

Portal turns the page you already have into that place, deterministically, with no model in the loop: the same URL always builds the same city, so a link you share opens the world you walked.

Quick start

npm install @three-ws/portal
import { fetchWorld, describeWorld } from '@three-ws/portal';

const { world } = await fetchWorld('example.com');
console.log(describeWorld(world));
// { host: 'example.com', sections: 2, doors: 1, tallest: { name: 'Example Domain', … } }

Put a live, walkable world on your own page:

import { mountPortal } from '@three-ws/portal';

mountPortal(document.querySelector('#stage'), { url: 'example.com', height: 560 });

Or without any JavaScript at all:

<iframe
	src="https://three.ws/portal?url=example.com&embed=1"
	width="100%" height="520" style="border:0;border-radius:14px"
	loading="lazy" title="Walk example.com in 3D"></iframe>

No key, no account, no wallet. fetchWorld reads the public three.ws Portal API, which fetches the page once and caches the result for an hour.

How a page becomes a place

The mapping is fixed, documented, and testable, which is what makes a world predictable enough to share:

On the pageIn the world
The page itselfA plaza with an obelisk carrying the title
Each h1/h2/h3 sectionA district with one building
How many words a section hasThe building's height, on a log scale
How many blocks it holdsThe building's footprint
Each linkA door on the building's wall. Same-site doors rebuild the world where they point; outside links open the page
Each imageA billboard beside the building, showing the real image
Each code blockA monolith: a tall, dark, emissive slab
theme-color, or the host nameThe palette, so a site looks like itself

Layout is a pure function with a seeded PRNG. Call it yourself on any outline:

import { buildWorld, collidersFor } from '@three-ws/portal/layout';

const world = buildWorld(outline);       // no network, no DOM, no three.js
const solids = collidersFor(world);      // circles, for your own player controller

API

ExportWhat it does
fetchWorld(url, opts?)Build (or read from cache) { world, outline, cached, stale }
fetchOutline(url, opts?)The structural read only: headings, weights, links, images
fetchWorldGlb(url, opts?)The world as a glTF binary (ArrayBuffer)
mountPortal(el, { url, height? })Mount a live walkable world into an element
embedSnippet(url, opts?)The same thing as an HTML string
describeWorld(world)A compact summary: districts, doors, tallest section
buildWorld(outline, opts?)The pure layout function
collidersFor(world)Collision circles for the solids in a world
paletteFor(outline)The deterministic palette for a site
PortalErrorEvery failure, carrying the API's own code

opts accepts endpoint (default https://three.ws/api/portal), fetch (bring your own), and signal.

Errors are typed rather than thrown as strings:

import { fetchWorld, PortalError } from '@three-ws/portal';

try {
	await fetchWorld('example.com/private');
} catch (err) {
	if (err instanceof PortalError && err.code === 'robots_disallowed') {
		console.log('That site asks crawlers to stay out, so Portal does.');
	}
}

Codes you can branch on: invalid_url, robots_disallowed, blocked_host, not_html, no_structure, too_large, unreachable, upstream_status, rate_limited.

CLI

npx @three-ws/portal three.ws                    # the world, summarized
npx @three-ws/portal three.ws --glb site.glb     # save it as a 3D file
npx @three-ws/portal three.ws --json world.json  # save the world document
npx @three-ws/portal three.ws --open             # print the walkable link
  three.ws
  24 districts · 1563 words · 42 doors (38 internal) · 2 billboards · 0 code monoliths

  ███████████···········    109w  Your 3D agent. In your room.
  █████████·············     66w  Your agents don't just talk. They trade.
  █████·················     20w  One tag. Any site.

MCP

An agent that fetches a page gets a wall of text. Over MCP it can ask for the page's shape instead.

claude mcp add portal -- npx -y @three-ws/portal portal-mcp
ToolReturns
walk_siteThe world: districts, doors (with targets), billboards, plus a walkable link and a GLB URL
site_shapeThe heading spine with the weight of each section. The cheap read

Both accept any public URL and honour the same manners as the API.

Manners

Portal is a crawler that runs on a person's request, and behaves like one:

  • It identifies itself as ThreeWSPortalBot/1.0 (+https://three.ws/portal).
  • It reads robots.txt first and honours a Disallow for that token with a clean robots_disallowed error. Rules are matched per RFC 9309: most specific user-agent group, longest matching rule, Allow wins a tie.
  • It reads a page once. Worlds are cached for an hour fleet-wide, so a link shared with a thousand people is one request to the origin.
  • It is bounded: 3 MB of HTML, a 12 second deadline, and an SSRF guard that re-resolves the host, refuses private address ranges, and pins the socket to the address it validated.

License

Apache-2.0. Built by three.ws.


Part of three.ws, the open-source platform for 3D AI agents. This repository is exported from the monorepo, which is the source of truth: open issues here, and send larger changes there.

Support the project

If portal saves you time, star it on GitHub. Stars are how other developers and AI agents find the repositories worth trusting, and they cost you one click.

Know someone who would use it? Post on X · Share on Bluesky · Share on LinkedIn · Submit to Hacker News · Share on Reddit

Built for AI agents too

Coding agents and LLM tooling can read this repo directly: AGENTS.md, llms.txt, llms-full.txt. Point an agent at https://github.com/nirholas/portal and it has the context it needs.

More from the same author

Contributors

Contributors

Star history

Star History Chart

↑↓ navigate openesc close