@zephyrcloudio/harness-server
Runtime-neutral Fetch HTTP handling for Ze Harness.
createServerHandler exposes injected SessionServices through validated HTTP routes and SSE.
import { createServerHandler } from '@zephyrcloudio/harness-server';
The Server does not choose a storage implementation or import Node.js. Node config discovery, trusted
plugin loading, and loopback hosting live in @zephyrcloudio/harness-server-node.
Long-lived event streams stay observable while nothing changes. A caught-up Session stream repeats
its session.cursor, a server stream repeats its server.cursor, and a Turn stream sends
turn.heartbeat naming the revision it already sent, at least every 15 seconds. All come from an
idle re-read of activity, which also recovers a missed wake.
The SSE projections are exported for embedding hosts that serve them outside the Fetch handler:
turnEventStream, sessionEventStream, and serverEventStream. The server stream wraps
SessionServices.observation.watchServerActivity and projects Turn rows through the same
functions as the Session stream, with one running-Turn memory per Session. A Session's first event
on a stream is always session.created: a Session row is session.created the first time the
stream sees that Session and session.title afterwards (storage stamps Session rows only on
creation and title changes), and a Turn row for a Session the stream has not announced yet is
preceded by a session.created read from the current Session, stamped one cursor before the
Turn's so a resume between the two replays the Turn. A stream
resumed after an existing Session was retitled therefore repeats session.created; consumers
upsert by session.id.
HarnessServerOptions accepts an optional providerAuthority service to serve model discovery and
provider connection management routes over the versioned v1 protocol. The
/v1/runtime/models/events and /v1/servers/:server/sessions/:sessionId/models/events routes are single-shot
current-revision checks, not long-lived subscriptions: each emits one revision event and closes.
Clients re-fetch the model snapshot endpoints for updates.
The portable Streamable HTTP MCP factory in ./mcp-host requires McpHostBackgroundWork. This host
port keeps an abort-ignoring fetch and any late response-body cleanup owned after the transport scope
closes. Node and Worker packages supply their platform-specific implementations.
MCP catalog pages validate their standard fields and accept additional server metadata.
A server may omit resources/list, resources/templates/list, or prompts/list:
a method-not-found response to the first page of one of those optional lists is
treated as an empty catalog for that list. Other request errors, tools/list
failures, and malformed catalog fields still fail discovery.
Dashboard
createServerHandler({ dashboard: { server } }) serves the dependency-free dashboard
(src/dashboard.html, inlined at build time by build/dashboard-asset.mjs) at exactly
GET /dashboard, before routing and without authentication. The Node daemon and
WorkerHarnessHost both serve this one asset. The page is a single self-contained HTML
document that does not assume its host:
- Server identity. The handler replaces
%ZE_HARNESS_SERVER%in<meta name="ze-harness-server">withdashboard.server. A page served without a valid name shows an error instead of guessing. - Relative requests. Every call is document-relative (
v1/servers/<server>/…,v1/logs), so the page works under any path prefix; at/dashboardthey resolve to/v1/…. - Login only when needed. On load the page lists Sessions with the tab's stored bearer
token, or none, and shows the token form only on
401. A host that vouches for its caller never shows it, and the sign-out chip appears only while a token is in use. When storage is unavailable (an opaque-origin frame) the token is kept in memory only. - Optional logs. The log panel hides once
v1/logsanswers404or503. - Headers.
content-security-policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'sha256-<page script>'; connect-src 'self'; base-uri 'none'; form-action 'none'. The script hash is computed at build time, so an embedder that appends its own script hashes toscript-src(CEF does for injected scripts) keeps the page's script running. There is noframe-ancestorsorX-Frame-Options, so any parent may frame the page. The page's API calls must be same-origin with it (connect-src 'self'). An embedder serving it from a custom URL scheme (such astap-harness://localhost) must register that scheme so its pages get a scheme/host/port origin; a page with an opaque origin has no'self'to match.
Theme
Every color, font, type size, weight, radius, shadow, and spacing value is a --zh-* custom
property on :root; the defaults are the built-in dark theme. The page has one color scheme
(dark) and takes no scheme parameter: an embedder restyles it, including a light palette and
--zh-color-scheme: light, through tokens. An embedding parent window posts:
iframe.contentWindow.postMessage(
{ type: 'ze-harness:theme', tokens: { '--zh-bg': '#ffffff', '--zh-text': '#1f2328' } },
'*',
);
The page applies only messages from its parent window, and only entries whose key is one of
the tokens below and whose value is a string; everything else is ignored. The message carries
theme, never data. Post it after the frame's load event; later messages override earlier
ones.
| Group | Tokens |
|---|---|
| Scheme | --zh-color-scheme (dark) |
| Colors | --zh-bg, --zh-panel, --zh-inset, --zh-border, --zh-text, --zh-dim, --zh-faint, --zh-accent, --zh-ok, --zh-warn, --zh-bad, --zh-steer |
| Fonts | --zh-font-sans, --zh-font-mono |
| Type size | --zh-text-10, -10-5, -11, -11-5, -12, -12-5, -13, -14, -15, -16, -17 |
| Type | --zh-weight-normal, --zh-weight-medium, --zh-weight-semibold, --zh-leading, --zh-leading-prose, --zh-tracking-label |
| Radius | --zh-radius-2, -3, -4, -5, -6, -8, --zh-radius-full |
| Shadow | --zh-shadow-card, --zh-shadow-focus, --zh-shadow-focus-thin |
| Spacing | --zh-space-1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -12, -14, -16, -18, -20, -24, -28, -32, -44, -80 |
Numbered type size, radius, and spacing tokens are named for their default pixel value
(--zh-text-10-5 is 10.5px). Tinted surfaces (selection, pills, error banners, focus ring)
mix the color tokens, so they follow a recolored palette.
Public TypeScript exports
AddSessionPluginAdminInputbearerUnauthorizedResponseClientWorkdirResolvercreateServerHandlerDaemonLogServiceApiDashboardOptionsHarnessServerHarnessServerOptionsisDashboardRequestmakeLazySessionPluginAdminmakeSessionPluginAdminProviderModelRefresherpublicSessionPluginInventoryRefreshSessionPluginAdminInputRuntimeAdminRuntimeAdminUnavailableserverEventStreamServerEventStreamInputSessionCatalogAdoptionStoresessionEventStreamSessionEventStreamInputSessionPluginActivationValueSessionPluginAdminSessionPluginAdminConflictSessionPluginAdminFailureSessionPluginAdminInvalidSessionPluginAdminStorageFailureSessionPluginAdminUnavailableSessionPluginRevisionValueSessionPluginsStatusValueSessionProjectResolverturnEventStreamTurnEventStreamInputverifyBearerRequest