Agent quick start
Hand this guide to a coding agent to install Oyster from npm, run it locally, install an App, connect over MCP and deploy.
You are an agent setting up Oyster for a person. Follow these steps in order. Run every command exactly as written, from the directory each step names. Compare its output with the expected output before you go on. When a step fails, read the “If it fails” notes under it before trying anything else.
Not sure Oyster fits the job? Is Oyster for you? compares it with Cloudflare Workers Builds before you install anything.
Before you start
Section titled “Before you start”-
Cloudflare plan: SQLite-backed Durable Objects are available on Workers Free; they do not by themselves require Paid. Cloudflare’s SQLite-backed Durable Objects limits list 30 seconds of CPU per request by default, configurable to 5 minutes. See Cloudflare’s Durable Objects limits and Durable Objects pricing.
-
Agent chat and AI Gateway: By default, Agent chat uses the Workers AI binding; it needs no separate provider key or Gateway setup. Prints uses your account’s default AI Gateway (created automatically); set
AI_GATEWAY_IDto use another. The routed models includeopenai/gpt-image-2and video IDsbytedance/seedance-2.0,bytedance/seedance-2.0-fast,bytedance/seedance-2.0-mini,google/veo-3.1,google/veo-3.1-fastandminimax/h3-max. -
Deployment plan: Step 7 needs Workers Paid because this deployment uses Containers for the Computer image; Cloudflare lists Containers as available on the Workers Paid plan. SQLite-backed Durable Objects alone do not require Paid.
What Oyster is
Section titled “What Oyster is”- Oyster hosts Apps on one Cloudflare Worker, or on celld, a Workers-compatible runtime you host yourself.
- Each App is an immutable Release built from an exact git commit. Its backend runs in its own Durable Object with its own SQLite, and it gets only the bindings it declares.
- People use Apps in a browser. Agents call the same App methods over HTTP or
through one MCP endpoint,
/mcp.
1. Check prerequisites
Section titled “1. Check prerequisites”You need Node 24 or newer, pnpm 11 or newer, Git, curl and openssl. You do not need a Cloudflare account until step 7. Docker is needed only in step 7, where Wrangler builds the Computer’s container image.
node --versionpnpm --versiongit --versionExpected output: a v24. or later Node version, pnpm 11 or newer, then any
git version 2.x line.
If it fails:
- Wrong Node: install Node 24 with your version manager, for example
mise use node@24ornvm install 24. - No pnpm, or an older version: install pnpm 11 with
npm install -g pnpm@11.
2. Create a project
Section titled “2. Create a project”Make a new directory, install Oyster, its Computer and Wrangler, and copy the example Worker out of the package. The local pnpm policy allows esbuild and workerd to install their native binaries and disables unneeded optional dependency scripts. Write it before installing so neither command stops for unapproved build scripts:
mkdir my-oystercd my-oystercat > pnpm-workspace.yaml <<'YAML'allowBuilds: "@google/genai": false "@mongodb-js/zstd": false esbuild: true exifreader: false node-liblzma: false protobufjs: false workerd: trueYAMLpnpm add @fungi.computer/oyster @fungi.computer/computer react@^19.2.0 react-dom@^19.2.0pnpm add -D wranglercp node_modules/@fungi.computer/oyster/examples/standalone/worker.ts .cp node_modules/@fungi.computer/oyster/examples/standalone/wrangler.jsonc .Expected output: pnpm lists the added package after each add, and ls now
shows node_modules, package.json, pnpm-workspace.yaml, worker.ts and
wrangler.jsonc.
Every later command runs from this my-oyster directory unless the step says
otherwise.
If it fails:
ERR_PNPM_IGNORED_BUILDS: check that the policy file above exists before runningpnpm add. If it names a new dependency not listed there, stop and review that package before allowing its scripts; do not treat exit 1 as success.cp: cannot stat …/examples/standalone/…: the install did not finish. Runpnpm add @fungi.computer/oyster @fungi.computer/computer react@^19.2.0 react-dom@^19.2.0again and read its first error.
3. Run a local Oyster
Section titled “3. Run a local Oyster”worker.ts and wrangler.jsonc are the whole Oyster. Run it under
wrangler dev on your machine, without its Computer container. Oyster serves an
HTTPS origin; --local-upstream presents your loopback requests to it at
https://oyster.test. Choose an owner key and keep this terminal open:
export OYSTER_OWNER_KEY=$(openssl rand -hex 24)export OYSTER_URL=http://127.0.0.1:8787echo "$OYSTER_OWNER_KEY" > .oyster-owner-keynpx wrangler dev --local --enable-containers=false --ip 127.0.0.1 --port 8787 \ --local-upstream oyster.test --upstream-protocol https \ --var OYSTER_OWNER_KEY:$OYSTER_OWNER_KEY --var OYSTER_ORIGIN:https://oyster.testExpected output: warnings that the AI and Artifacts bindings reach remote
resources, a bindings table listing env.REGISTRY (OysterRegistry),
env.INSTANCES (StandaloneInstance), env.COMPUTER (ComputerDO) and the rest,
then Ready on http://127.0.0.1:8787. Locally there is no Computer, git storage
or Workers AI; steps 3 to 5 need none of them.
Keep it running. Open a second terminal in my-oyster, set the same two
variables there, and check that Oyster answers:
export OYSTER_OWNER_KEY=$(cat .oyster-owner-key)export OYSTER_URL=http://127.0.0.1:8787curl -s -o /dev/null -w '%{http_code}\n' $OYSTER_URL/logincurl -s -o /dev/null -w '%{http_code}\n' $OYSTER_URL/api/installationscurl -s -H "Authorization: Bearer $OYSTER_OWNER_KEY" $OYSTER_URL/api/installationsExpected output: 200, then 401 (no key), then [] (no Apps installed yet).
If it fails:
-
Address already in use: another process holds port 8787. Pick another port and use it in both--portandOYSTER_URL. -
curlprints000: the server is not ready yet. Wait for theReady online. -
401with the key: the two terminals hold different keys. SetOYSTER_OWNER_KEYfrom.oyster-owner-keyin both. -
An agent runtime that cannot keep a process in the foreground starts the same command in the background with its output in a log, then waits until the
/logincheck prints200:TERMINAL npx wrangler dev --local --enable-containers=false --ip 127.0.0.1 --port 8787 \--local-upstream oyster.test --upstream-protocol https \--var OYSTER_OWNER_KEY:$OYSTER_OWNER_KEY --var OYSTER_ORIGIN:https://oyster.test \> oyster-dev.log 2>&1 &
To stop the server, press Ctrl-C in its terminal. A background server runs as
several processes (npm exec, wrangler.js and workerd); stop them all by
ending wrangler.js, then check that /login prints 000:
pkill -f 'wrangler[.]js dev .*--port 8787'4. Add an App
Section titled “4. Add an App”An App is a git repository with a package.json that declares it under
fungi.app. Make the smallest one, a single committed HTML page:
mkdir -p hello-app/bundlecat > hello-app/package.json <<'EOF'{ "name": "hello-app", "private": true, "fungi": { "app": { "displayName": "Hello", "capabilities": [] } }}EOFcat > hello-app/bundle/index.html <<'EOF'<!doctype html><title>Hello</title><h1>Hello from my first Oyster App</h1>EOFgit -C hello-app init -qgit -C hello-app add .git -C hello-app -c user.name=Oyster -c user.email=oyster@example.invalid \ commit -qm "Hello App"Then pack that commit into a Release and install it as installation hello with
@fungi.computer/oyster/pack:
cat > install-hello.mjs <<'EOF'import { execFileSync } from "node:child_process";import { mkdtemp, rm } from "node:fs/promises";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import process from "node:process";import { installRelease, packRelease } from "@fungi.computer/oyster/pack";
const source = resolve("hello-app");const commit = execFileSync("git", ["-C", source, "rev-parse", "HEAD"], { encoding: "utf8",}).trim();const out = await mkdtemp(join(tmpdir(), "oyster-hello-"));try { await packRelease({ appId: "app_hello", sourceDirectory: source, sourceRevision: `git:${commit}`, recipe: { kind: "bundled-html", entry: "bundle/index.html", assets: { maxManifestBytes: 65_536, maxObjects: 16, maxObjectBytes: 1_048_576, maxTotalBytes: 4_194_304, }, assetBase: "./", backend: null, maxOutputBytes: 4_194_304, }, outDirectory: join(out, "release"), }); const response = await installRelease({ directory: join(out, "release"), url: process.env.OYSTER_URL, key: process.env.OYSTER_OWNER_KEY, installationId: "hello", }); console.log(`installed hello: ${response.status} ${await response.text()}`);} finally { await rm(out, { recursive: true, force: true });}EOFnode install-hello.mjscurl -s -o /dev/null -w '%{http_code}\n' \ -H "Authorization: Bearer $OYSTER_OWNER_KEY" $OYSTER_URL/apps/hello/Expected output: one line starting
installed hello: 201 {"installationId":"hello", and containing
"status":"enabled", then 200. Running node install-hello.mjs again prints
installed hello: 200: the installation already exists, and Oyster switches it
to the Release just packed.
A person can open $OYSTER_URL/login, sign in with the owner key, and open
Hello from the installation list at $OYSTER_URL/.
If it fails:
installed hello: 401:OYSTER_OWNER_KEYin this terminal differs from the server’s. Set it from.oyster-owner-key.fetch failedorECONNREFUSED: the server from step 3 is not running.- The build reads committed files only. To install an edit, commit it in
hello-appand runnode install-hello.mjsagain; Oyster switches the installation to the new Release and keeps its data.
An App with a backend, typed methods and its own SQLite adds
fungi.backendDefinition and a backend entry to its recipe. The
Notes example App
is one, and the Oyster README’s “Pack an App” and “Build App Releases” sections
describe the recipe.
5. Connect over MCP
Section titled “5. Connect over MCP”/mcp serves every installed App to MCP clients through one execute tool, in
which each App’s methods are env.<APP>. The owner key signs in. For Claude
Code:
claude mcp add --transport http oyster "$OYSTER_URL/mcp" \ --header "Authorization: Bearer $OYSTER_OWNER_KEY"To check /mcp without an MCP client, open a session and list its tools:
MCP=(-H "Authorization: Bearer $OYSTER_OWNER_KEY" -H 'content-type: application/json' -H 'accept: application/json, text/event-stream')SESSION=$(curl -s "${MCP[@]}" -D - -o /dev/null \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \ $OYSTER_URL/mcp | grep -i '^mcp-session-id' | cut -d' ' -f2 | tr -d '\r')curl -s "${MCP[@]}" -H "mcp-session-id: $SESSION" -H 'mcp-protocol-version: 2025-11-25' \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' $OYSTER_URL/mcpcurl -s "${MCP[@]}" -H "mcp-session-id: $SESSION" -H 'mcp-protocol-version: 2025-11-25' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' $OYSTER_URL/mcpExpected output: the last command prints an event: message block whose data:
line contains "name":"execute".
If it fails:
SESSIONis empty: theinitializecall was refused. Check the key as in step 3.- The copied Worker already includes
POST /api/keys. Use the owner key to create a separate key for each agent; no extra Worker is needed.
6. Add Git
Section titled “6. Add Git”Git is already part of the host: people and agents clone, fetch and push
App repositories with ordinary git at /git/<installation>/<repo>.git, with
repositories in Cloudflare Artifacts. Artifacts runs only once the Worker is
deployed in step 7. There, give each agent its own key:
curl -s -X POST -H "Authorization: Bearer $OYSTER_OWNER_KEY" \ -H 'content-type: application/json' \ -d '{"name":"morel","grants":[{"capability":"apps.use","targets":{"kind":"all"}}]}' \ $OYSTER_URL/api/keysExpected output: JSON with a plaintext field starting oyk_. Oyster shows it
once; that key signs in to git and to /mcp.
7. Deploy to Cloudflare Workers
Section titled “7. Deploy to Cloudflare Workers”This step needs a Cloudflare account on Workers Paid with Containers, Artifacts, Workers AI and Workflows enabled, and Docker running on this machine. Ask the person before you deploy anything.
wrangler.jsonc names the Worker oyster, its R2 bucket oyster-releases and
its Artifacts namespace oyster. The Worker’s address is
https://oyster.<your-subdomain>.workers.dev, or a custom domain:
npx wrangler loginnpx wrangler r2 bucket create oyster-releasesexport OYSTER_URL=https://oyster.<your-subdomain>.workers.devnpx wrangler deploy --var OYSTER_ORIGIN:$OYSTER_URLprintf '%s' "$OYSTER_OWNER_KEY" | npx wrangler secret put OYSTER_OWNER_KEYExpected output: wrangler deploy builds the Computer image, uploads it and
prints the deployed https:// URL, and wrangler secret put reports the secret
uploaded.
Then install Fungi’s seven starter Apps, each built on this Oyster’s Computer from its own repository:
npx oyster install-appsExpected output: a source recorded line per App, Building … on the Computer
and Installed … lines (two builds at a time, Tourtois first), a timing table,
and Installed 7 Apps. Open $OYSTER_URL/login with the owner key to see the
Tourtois desktop.
To keep OYSTER_ORIGIN across later deploys, set vars.OYSTER_ORIGIN in
wrangler.jsonc; every deploy replaces the Worker’s variables.
If it fails:
R2 bucket … already exists: use that bucket, or pick a new name and put it inwrangler.jsonc.- A secret prompt on stdin: pipe the key as shown; never type it into a log.
- Bindings or migrations refused: deploy the copied
wrangler.jsoncunchanged except forvarsand the bucket name. dockernot found or not running: start Docker; Wrangler builds the Computer image fromnode_modules/@fungi.computer/computer/Dockerfile.- A build
failedininstall-apps: run it again; finished Apps are kept and replaced in place.
Where to go next
Section titled “Where to go next”- The Oyster README (
node_modules/@fungi.computer/oyster/README.md) explains the host, Apps, capabilities, MCP and the shell in depth. - oyster.shiit.app runs this same host with the seven starter Apps.
- Oyster is built from the other Fungi packages, each its own repository under
github.com/fungi-computer and on npm as
@fungi.computer/<name>. The README’s “Part of Fungi” section lists them.