Skip to content
Agent quick start

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.

  • 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_ID to use another. The routed models include openai/gpt-image-2 and video IDs bytedance/seedance-2.0, bytedance/seedance-2.0-fast, bytedance/seedance-2.0-mini, google/veo-3.1, google/veo-3.1-fast and minimax/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.

  • 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.

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.

TERMINAL
node --version
pnpm --version
git --version

Expected 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@24 or nvm install 24.
  • No pnpm, or an older version: install pnpm 11 with npm install -g pnpm@11.

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:

TERMINAL
mkdir my-oyster
cd my-oyster
cat > pnpm-workspace.yaml <<'YAML'
allowBuilds:
"@google/genai": false
"@mongodb-js/zstd": false
esbuild: true
exifreader: false
node-liblzma: false
protobufjs: false
workerd: true
YAML
pnpm add @fungi.computer/oyster @fungi.computer/computer react@^19.2.0 react-dom@^19.2.0
pnpm add -D wrangler
cp 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 running pnpm 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. Run pnpm add @fungi.computer/oyster @fungi.computer/computer react@^19.2.0 react-dom@^19.2.0 again and read its first error.

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:

TERMINAL
export OYSTER_OWNER_KEY=$(openssl rand -hex 24)
export OYSTER_URL=http://127.0.0.1:8787
echo "$OYSTER_OWNER_KEY" > .oyster-owner-key
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

Expected 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:

TERMINAL
export OYSTER_OWNER_KEY=$(cat .oyster-owner-key)
export OYSTER_URL=http://127.0.0.1:8787
curl -s -o /dev/null -w '%{http_code}\n' $OYSTER_URL/login
curl -s -o /dev/null -w '%{http_code}\n' $OYSTER_URL/api/installations
curl -s -H "Authorization: Bearer $OYSTER_OWNER_KEY" $OYSTER_URL/api/installations

Expected 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 --port and OYSTER_URL.

  • curl prints 000: the server is not ready yet. Wait for the Ready on line.

  • 401 with the key: the two terminals hold different keys. Set OYSTER_OWNER_KEY from .oyster-owner-key in 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 /login check prints 200:

    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:

TERMINAL
pkill -f 'wrangler[.]js dev .*--port 8787'

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:

TERMINAL
mkdir -p hello-app/bundle
cat > hello-app/package.json <<'EOF'
{
"name": "hello-app",
"private": true,
"fungi": { "app": { "displayName": "Hello", "capabilities": [] } }
}
EOF
cat > hello-app/bundle/index.html <<'EOF'
<!doctype html>
<title>Hello</title>
<h1>Hello from my first Oyster App</h1>
EOF
git -C hello-app init -q
git -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:

TERMINAL
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 });
}
EOF
node install-hello.mjs
curl -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_KEY in this terminal differs from the server’s. Set it from .oyster-owner-key.
  • fetch failed or ECONNREFUSED: the server from step 3 is not running.
  • The build reads committed files only. To install an edit, commit it in hello-app and run node install-hello.mjs again; 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.

/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:

TERMINAL
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:

TERMINAL
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/mcp
curl -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/mcp

Expected output: the last command prints an event: message block whose data: line contains "name":"execute".

If it fails:

  • SESSION is empty: the initialize call 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.

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:

TERMINAL
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/keys

Expected output: JSON with a plaintext field starting oyk_. Oyster shows it once; that key signs in to git and to /mcp.

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:

TERMINAL
npx wrangler login
npx wrangler r2 bucket create oyster-releases
export OYSTER_URL=https://oyster.<your-subdomain>.workers.dev
npx wrangler deploy --var OYSTER_ORIGIN:$OYSTER_URL
printf '%s' "$OYSTER_OWNER_KEY" | npx wrangler secret put OYSTER_OWNER_KEY

Expected 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:

TERMINAL
npx oyster install-apps

Expected 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 in wrangler.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.jsonc unchanged except for vars and the bucket name.
  • docker not found or not running: start Docker; Wrangler builds the Computer image from node_modules/@fungi.computer/computer/Dockerfile.
  • A build failed in install-apps: run it again; finished Apps are kept and replaced in place.
  • 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.