Package Exports
- @abide/abide/build
- @abide/abide/bundle/BundleMenu
- @abide/abide/bundle/BundleMenuItem
- @abide/abide/bundle/BundleWindow
- @abide/abide/bundle/bundled
- @abide/abide/bundle/onMenu
- @abide/abide/compile
- @abide/abide/mcp/createMcpServer
- @abide/abide/preload
- @abide/abide/resolver-plugin
- @abide/abide/server/AppModule
- @abide/abide/server/DELETE
- @abide/abide/server/GET
- @abide/abide/server/HEAD
- @abide/abide/server/InspectorContext
- @abide/abide/server/PATCH
- @abide/abide/server/POST
- @abide/abide/server/PUT
- @abide/abide/server/agent
- @abide/abide/server/appDataDir
- @abide/abide/server/cookies
- @abide/abide/server/env
- @abide/abide/server/error
- @abide/abide/server/json
- @abide/abide/server/jsonl
- @abide/abide/server/prompts/definePrompt
- @abide/abide/server/prompts/renderPromptTemplate
- @abide/abide/server/reachable
- @abide/abide/server/redirect
- @abide/abide/server/request
- @abide/abide/server/rpc/defineVerb
- @abide/abide/server/server
- @abide/abide/server/socket
- @abide/abide/server/sockets/defineSocket
- @abide/abide/server/sse
- @abide/abide/shared/HttpError
- @abide/abide/shared/cache
- @abide/abide/shared/createSubscriber
- @abide/abide/shared/health
- @abide/abide/shared/html
- @abide/abide/shared/log
- @abide/abide/shared/online
- @abide/abide/shared/page
- @abide/abide/shared/pending
- @abide/abide/shared/refreshing
- @abide/abide/shared/snippet
- @abide/abide/shared/trace
- @abide/abide/shared/url
- @abide/abide/shared/withJsonSchema
- @abide/abide/test/assertAgentFrameConformance
- @abide/abide/test/createScriptedSurface
- @abide/abide/test/createTestApp
- @abide/abide/tsconfig
- @abide/abide/ui-plugin
- @abide/abide/ui/dom/anchorCursor
- @abide/abide/ui/dom/appendSnippet
- @abide/abide/ui/dom/appendStatic
- @abide/abide/ui/dom/appendText
- @abide/abide/ui/dom/appendTextAt
- @abide/abide/ui/dom/applyResolved
- @abide/abide/ui/dom/attach
- @abide/abide/ui/dom/attr
- @abide/abide/ui/dom/awaitBlock
- @abide/abide/ui/dom/cloneStatic
- @abide/abide/ui/dom/each
- @abide/abide/ui/dom/eachAsync
- @abide/abide/ui/dom/hydrate
- @abide/abide/ui/dom/mergeProps
- @abide/abide/ui/dom/mount
- @abide/abide/ui/dom/mountChild
- @abide/abide/ui/dom/mountSlot
- @abide/abide/ui/dom/on
- @abide/abide/ui/dom/outlet
- @abide/abide/ui/dom/readCall
- @abide/abide/ui/dom/restProps
- @abide/abide/ui/dom/skeleton
- @abide/abide/ui/dom/spreadAttrs
- @abide/abide/ui/dom/spreadProps
- @abide/abide/ui/dom/switchBlock
- @abide/abide/ui/dom/text
- @abide/abide/ui/dom/tryBlock
- @abide/abide/ui/dom/when
- @abide/abide/ui/effect
- @abide/abide/ui/enterScope
- @abide/abide/ui/exitScope
- @abide/abide/ui/navigate
- @abide/abide/ui/outbox
- @abide/abide/ui/remoteProxy
- @abide/abide/ui/renderToStream
- @abide/abide/ui/router
- @abide/abide/ui/runtime/enterRenderPass
- @abide/abide/ui/runtime/escapeKey
- @abide/abide/ui/runtime/exitRenderPass
- @abide/abide/ui/runtime/nextBlockId
- @abide/abide/ui/scope
- @abide/abide/ui/socketProxy
- @abide/abide/ui/startClient
- @abide/abide/ui/tail
Readme
abide
One typed declaration fans out to HTTP, a CLI, an MCP tool, and an OpenAPI spec — the bundler swaps the runtime per side.
abide is an isomorphic framework on Bun where you write a function once and it serves every consumer: a browser fetch, an in-process SSR call, a CLI subcommand, an MCP tool, an OpenAPI operation. The same callable keeps its name and behaviour on both sides — the bundler decides whether it runs the real handler or a network proxy. Built for humans and machines.
- One direct dependency (
typescript);tailwindcss+bun-plugin-tailwindare optional peers. Single runtime: Bun ≥ 1.3.0.
Quick start
bunx abide scaffold my-app # scaffolds, installs deps, and starts devOr read the full feature tour in the kitchen-sink example:
git clone https://github.com/briancray/abide
cd abide/examples/kitchen-sink
bun install
bun run devRPCs
An RPC is one export per file under src/server/rpc/. The file path is the
URL; the export name is the verb. A Standard Schema (zod / valibot / arktype,
unadapted) validates the args and projects the same shape into the MCP tool,
the CLI flags, and the OpenAPI operation.
// src/server/rpc/getMessages.ts
import { GET } from '@abide/abide/server/GET'
import { json } from '@abide/abide/server/json'
import { z } from 'zod'
import { recent } from '../../chatState.ts'
const inputSchema = z.object({ room: z.string(), limit: z.coerce.number().default(20) })
export const getMessages = GET(({ room, limit }) => json(recent(room).slice(-limit)), {
inputSchema,
})One declaration, every surface:
getMessages = GET(fn, { inputSchema })
│
┌─────────────┬─────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
SSR call browser MCP tool CLI sub- OpenAPI
cache(fn)() fetch (read) command operation
proxyA schema unlocks the CLI for every verb and MCP for read-only verbs (GET /
HEAD); a mutating verb never auto-exposes to MCP — it needs an explicit
clients: { mcp: true }. Consume the verb four ways: cache(getMessages)(args)
in-process (warm SSR hydration), the swapped fetch proxy in the browser,
getMessages.raw(args) for the untouched Response, and
getMessages.stream(args) to iterate a jsonl/sse body.
Query args arrive as strings — wrap numeric/boolean fields in
z.coerce.*. The per-verbtimeout(504 on every surface) is distinct from the client-sideABIDE_CLIENT_TIMEOUT.
Sockets
A socket is one broadcast topic per file under src/server/sockets/. A
Socket<T> is an isomorphic AsyncIterable<T>; every socket multiplexes onto
one WebSocket at /__abide/sockets.
// src/server/sockets/chat.ts
import { socket } from '@abide/abide/server/socket'
import { z } from 'zod'
const schema = z.object({ id: z.string(), from: z.string(), text: z.string(), at: z.number() })
// retain the last 100 frames; evict any older than an hour
export const chat = socket({ schema, tail: 100, ttl: 3_600_000 })
export type ChatMessage = z.infer<typeof schema>It also has an HTTP face for clients that can't speak the multiplex (the CLI
and MCP): GET /__abide/sockets/chat returns the retained tail, and
POST /__abide/sockets/chat publishes — gated by clientPublish (default off,
so browsers publish through a validating verb instead).
Components — the full template
A .abide component pulls the verb and the socket above into one page and
exercises the template grammar. scope, props, effect, html, and
snippet are ambient — no import needed.
<script>
import { cache } from '@abide/abide/shared/cache'
import { tail } from '@abide/abide/ui/tail'
import { getMessages } from '$server/rpc/getMessages.ts'
import { publishChat } from '$server/rpc/publishChat.ts'
import { chat } from '$server/sockets/chat.ts'
import Avatar from '$ui/Avatar.abide'
const { room } = props()
// warm on the server, live on the client
const history = scope().computed(() => cache(getMessages)({ room }))
const latest = scope().computed(() => tail(chat))
let from = scope().state('alice')
let text = scope().state('')
let pinned = scope().state(false)
let view = scope().state('all')
async function send() {
await publishChat({ from, text })
text = ''
}
</script>
<template name="line" args={message}>
<li><Avatar name={message.from} /> <b>{message.from}</b>: {message.text}</li>
</template>
<form onsubmit={send}>
<input bind:value={from} placeholder="name" />
<input bind:value={text} placeholder="message" />
<label><input type="checkbox" bind:checked={pinned} /> pin</label>
<label><input type="radio" bind:group={view} value="all" /> all</label>
<label><input type="radio" bind:group={view} value="mine" /> mine</label>
<button disabled={!text}>send</button>
</form>
{#if latest}
<p>latest from {latest.from}</p>
{:else}
<p>no messages yet</p>
{/if}
{#switch view}
{:case 'mine'}<p>showing your messages</p>
{:default}<p>showing every message</p>
{/switch}
{#await history}
<p>loading…</p>
{:then data}
<ul>
{#for message, i of data by message.id}
{i}. {line(message)}
{/for}
</ul>
{:catch reason}
<p>failed: {reason.message}</p>
{/await}
<style>
form { display: flex; gap: 0.5rem; }
</style>MIT