Server-side glue: mount an aihu component server-side and let an MCP client drive it through the agent-service live-dispatch gate, forwarding approved invocations to a browser bridge.
createAgentServerfunction createAgentServer(options: AgentServerOptions): AgentServercreateBridgeClientfunction createBridgeClient(options: BridgeClientOptions): BridgeClientStart a browser bridge client.
createComponentMcpServerfunction createComponentMcpServer( agentServer: AgentServer, metas: AgentMetadata[] = getAllAgentMetadata(), ): ServeropaqueActionIdfunction opaqueActionId(tag: string, member: string): stringDeterministic, stable opaque ID for an agent-exposed member — identical to the compiler's `opaque_member_id(tag, member)`.
opaqueActionIdForToolfunction opaqueActionIdForTool(toolName: string): string | nullOpaque ID directly from a `"<tag>/<member>"` tool name; null if malformed.
parseToolNamefunction parseToolName(toolName: string): { tag: string; member: string } | nullSplit a `"<tag>/<member>"` tool name.
serveComponentMcpasync function serveComponentMcp( agentServer: AgentServer, metas?: AgentMetadata[], ): Promise<Server>Convenience: create the component MCP server and connect it over stdio (the same transport `@aihu/mcp` uses).
BRIDGE_PROTOCOL_VERSIONconst BRIDGE_PROTOCOL_VERSIONAgentDispatcherinterface AgentDispatcher {
readonly tag: string
/** opaqueId → action invoker (called with the positional args array). */
readonly actions: Record<string, (args: unknown[]) => unknown>
/** opaqueId → read accessor (current signal value). */
readonly reads: Record<string, () => unknown>
/** opaqueId → write accessor. */
readonly writes: Record<string, (value: unknown) => void>
}The compiler-emitted `__agentDispatcher` shape (client artifact, T1).
AgentServerinterface AgentServer {
/** The underlying agent-service (exposes `handleToolCall`, `asMiddleware`). */
readonly service: AgentService
/** The `MountScope` of the server-mounted component. */
readonly mount: MountScope
/**
* Run a tool call through the full agent-service gate. On authorization, the
* dispatch is applied to the server-mounted binding AND, if a browser bridge
* is attached, forwarded to it as a {@link BridgeInvokeMessage}.
*
* Returns the agent-service envelope: `{ result }` on success, or
* `{ error, code, jsonrpc }` on a gate rejection (404/401/403/429). The HTTP
* code is never mutated here — the gate is the sole policy authority.
*/
callTool(toolName: string, params: unknown, ctx?: RequestContext): Promise<unknown>
/** Current serialized state of the server-mounted component. */
serialize(): Snapshot
/**
* Attach a browser bridge channel. Approved invocations are forwarded to it;
* `result`/`error`/`snapshot` frames from it resolve pending `callTool`
* promises. Returns a detach function.
*
* The channel MUST complete the `hello` handshake (see
* {@link BRIDGE_PROTOCOL_VERSION}) before anything is delegated to it. Until
* it does, `hello` is the only frame it may send, and `callTool` refuses to
* forward — an unverified channel never becomes the execution authority.
* Attaching a new channel always resets this state; verified status is never
* inherited from a previous peer.
*/
attachBridge(channel: BridgeChannel): () => void
/**
* Build the MCP `Server` (stdio-ready) exposing each component action as an
* MCP tool backed by `callTool`. Lazily constructed; the SDK is imported only
* when this is called. See {@link createComponentMcpServer}.
*/
// (the MCP server factory is a separate export to keep the SDK import lazy)
/** Tear down: dispose the mount + detach any bridge. */
dispose(): void
}AgentServerOptionsinterface AgentServerOptions {
/** The component to mount + drive. */
target: AgentServerTarget
/**
* Optional host factory for the server-side mount. When omitted,
* `createAgentServer` stands up an internal jsdom-backed global DOM (if the
* runtime has none) and mounts into a fresh detached `<div>` — so a plain
* Bun/Node consumer needs no DOM glue at all. Provide this only to mount into
* a specific host element (an explicit factory always wins).
*/
createHost?: () => Element
/** Auth plugin — forwarded to `createAgentService` for `$scope` checks. */
authPlugin?: AuthPlugin
/** Rate-limit plugin — forwarded to `createAgentService`. */
rateLimitPlugin?: RateLimitPlugin
/**
* Per-request auth resolver — forwarded to `createAgentService.asMiddleware`.
* Lets scoped/rate-limited tools be reachable over the bundled HTTP path.
*/
resolveAuth?: AgentServiceOptions['resolveAuth']
/**
* Auth-discovery URL forwarded to `createAgentService` — included in every
* 401 envelope so a refused agent knows where to obtain a credential (e.g.
* the deployment's `/.well-known/oauth-protected-resource`). Informational
* only; never a policy input.
*/
authDiscoveryUrl?: AgentServiceOptions['authDiscoveryUrl']
/**
* How long (ms) `callTool` waits for an attached bridge channel to complete
* its `hello` handshake before refusing to delegate to it (503
* `BRIDGE_UNVERIFIED`). Defaults to 1000ms.
*
* The wait exists only to absorb the attach/connect race over a real socket;
* it is not a grace period. A channel that sends a MISMATCHED or non-numeric
* protocol version is rejected immediately, without waiting out this timeout.
*/
bridgeHandshakeTimeoutMs?: number
}AgentServerTargetinterface AgentServerTarget {
/** arbor render tree (root `Node`) to mount server-side. */
node?: Node
/** Compiler-emitted `__agentBinding` server export for the component. */
agentBinding?: AgentBindingSpec
/** A pre-built `MountScope` (its binding is already registered). */
mount?: MountScope
}The thing being driven.
BridgeChannelinterface BridgeChannel {
/** Send one (already-serialized) message frame to the peer. */
send(data: string): void
/** Register a frame handler. Returns an unsubscribe function. */
onMessage(handler: (data: string) => void): () => void
/** Register a close handler. Returns an unsubscribe function. */
onClose(handler: () => void): () => void
/** True while the channel can still deliver frames. */
readonly connected: boolean
}The minimal duplex transport the bridge needs.
BridgeClientinterface BridgeClient {
/** Stop handling frames. Does not close the underlying channel. */
dispose(): void
}A running bridge client.
BridgeClientOptionsinterface BridgeClientOptions {
/** The compiler-emitted `__agentDispatcher` for the mounted component. */
dispatcher: AgentDispatcher
/** Duplex channel to the server (a `ws` WebSocket in production). */
channel: BridgeChannel
/**
* Optional snapshot source (e.g. the mount's `serialize`). When provided, a
* `snapshot` frame is pushed after each successful invocation so the server
* and any read-only viewer reflect the visible instance's new state.
*/
serialize?: () => unknown
}BridgeErrorMessageinterface BridgeErrorMessage {
type: 'error'
callId: string
message: string
}Client → server.
BridgeHelloMessageinterface BridgeHelloMessage {
type: 'hello'
protocol: number
}Handshake, client → server, sent once on connect.
BridgeInvokeMessageinterface BridgeInvokeMessage {
type: 'invoke'
/** Correlates the eventual `result`/`error`/`snapshot` reply. */
callId: string
/** Opaque action identifier the client maps to a concrete action. */
opaqueActionId: string
/** Positional arguments for the action. */
args: unknown[]
}Server → client.
BridgeResultMessageinterface BridgeResultMessage {
type: 'result'
callId: string
result: unknown
}Client → server.
BridgeSnapshotMessageinterface BridgeSnapshotMessage {
type: 'snapshot'
callId?: string
snapshot: Snapshot
}Client → server (or server → a read-only viewer).
BridgeClientMessagetype BridgeClientMessage =
| BridgeHelloMessage
| BridgeResultMessage
| BridgeErrorMessage
| BridgeSnapshotMessageAny message a client may send to the server over the bridge.
BridgeServerMessagetype BridgeServerMessage = BridgeInvokeMessageAny message the server may send to a client over the bridge.