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.
createBridgeNonceStorefunction createBridgeNonceStore(): BridgeNonceStorecreateComponentMcpServerfunction 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).
verifyBridgeUpgradefunction verifyBridgeUpgrade( req: Request, options: VerifyBridgeUpgradeOptions, ): BridgeUpgradeVerdictDecide whether an HTTP upgrade request for the capability bridge should be accepted, based on its `Origin` header.
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
/** Issue a single-use nonce for a forthcoming bridge handshake. */
issueBridgeNonce(ttlMs?: number): BridgeNonce
/** Revoke a verified bridge identity (or its exact grant) and cancel its pending calls. */
revokeBridgeSession(identity: string, scope?: { readonly grantVersion: string }): 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']
actorResolver?: ActorResolver
authorizeDataRead?: CapabilityGrantResolver
/** Maximum duration for each injected security hook and pending bridge call. Defaults to 5000ms. */
securityHookTimeoutMs?: number
/**
* 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']
/** Verify a bridge token and return its stable identity/grant binding. Missing verifier denies attachment. */
verifyBridgeSession?: (token: string) => BridgeVerifiedSession | Promise<BridgeVerifiedSession>
/**
* 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
/** Maximum duration for a bridge invocation reply. Defaults to 5000ms. */
bridgeCallTimeoutMs?: number
/** Maximum in-flight bridge calls for one attachment. Defaults to 64. */
maxPendingBridgeCalls?: number
/** Maximum in-flight bridge calls across the server. Defaults to 1024. */
maxPendingBridgeCallsTotal?: number
/** Maximum bridge calls from admission through final result authorization. Defaults to 1024. */
maxInFlightBridgeCalls?: number
/** Maximum admitted calls for one verified actor organization. Defaults to 64. Calls without an actor share one bucket. */
maxInFlightBridgeCallsPerTenant?: number
/** Bounds projected bridge read results. Defaults to depth 32 and 10,000 total nodes. */
projectionLimits?: CapabilityProjectionLimits
/** Revocation memory lifetime. Defaults to 24 hours; host session storage remains authoritative. */
bridgeRevocationTtlMs?: number
/** Maximum retained revocation bindings. New handshakes fail with BRIDGE_REVOCATION_STORE_FULL while full. Defaults to 10,000. */
bridgeRevocationMaxEntries?: number
/** Re-check the bound session/grant before every invocation. */
reauthorizeBridgeInvoke?: (binding: {
readonly sessionToken?: string
readonly identity: string
readonly grantVersion?: string
/** Actor resolved by the service gate for this invocation, if verified. */
readonly actor?: Actor
}) => BridgeVerifiedSession | false | Promise<BridgeVerifiedSession | false>
/** Optional host-side diagnostics for rejected bridge frames. Disabled when omitted. */
onBridgeDiagnostic?: (diagnostic: {
readonly event: string
readonly detail?: unknown
}) => void | Promise<void>
}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
/** Close the underlying peer after a protocol violation, when supported by the transport. */
close?(): 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
/** Server-issued single-use handshake nonce. */
nonce?: string
/** Verified session credential, bound to this bridge peer. */
sessionToken?: string
/** Identity returned by the server's session verifier. */
sessionIdentity?: string
/** Current actor/grant version, rechecked by the server on every invoke. */
grantVersion?: string
/**
* 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
/** Optional host-only diagnostic callback. Its raw error text is never sent over the bridge. */
onDiagnostic?: (diagnostic: { code: string; message: string }) => void
}BridgeErrorMessageinterface BridgeErrorMessage {
type: 'error'
callId: string
code?: string
message: string
}Client → server.
BridgeHelloMessageinterface BridgeHelloMessage {
type: 'hello'
protocol: number
sessionToken?: string
sessionIdentity?: string
nonce?: string
grantVersion?: string
}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.
BridgeNonceinterface BridgeNonce {
readonly nonce: string
readonly expiresAt: number
}Single-use, short-lived bridge handshake nonces.
BridgeNonceStoreinterface BridgeNonceStore {
issue(ttlMs: number | undefined, connectionId: string): BridgeNonce
consume(value: unknown, connectionId: string): boolean
}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).
BridgeVerifiedSessioninterface BridgeVerifiedSession {
readonly identity: string
readonly grantVersion?: string
}VerifyBridgeUpgradeOptionsinterface VerifyBridgeUpgradeOptions {
/**
* Origins allowed to open the capability-bridge WebSocket, exact string
* match against the `Origin` request header (e.g. `http://localhost:5108`).
* There is no wildcard support — a bridge that trusts every origin is the
* vulnerability this function exists to close.
*/
allowedOrigins: readonly string[]
}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.
BridgeUpgradeVerdicttype BridgeUpgradeVerdict = { ok: true } | { ok: false; status: number; reason: string }