Widget Context Protocol
An open standard for widget–dashboard communication. Any server — a Docker container, a local process, or a remote service — can expose a compact UI widget and register its capabilities with any WCP-compatible host dashboard.
WCP is deliberately modelled on Anthropic's Model Context Protocol (MCP): a simple, discoverable interface that any compliant host can speak. A WCP widget exposes a small set of HTTP endpoints and, optionally, communicates with its host via the browser's postMessage API.
Protocol Overview
A WCP widget is any HTTP server that implements the mandatory endpoint set. The host dashboard discovers it by fetching its manifest, embeds it in an iframe, and routes postMessage events between widget and dashboard.
Dashboard host (port 3737)
|
+-- GET /api/widget-manifest?url=http://widget:3740
| +-- fetches GET http://widget:3740/widget/wcp <- WCP manifest
|
+-- Embeds http://widget:3740/widget/ in an <iframe>
|
+-- Listens for window.postMessage events
+-- wcp:open-window, wcp:open-tab, wcp:copy-to-clipboard ...
The widget's URL is the only thing the dashboard needs. Everything else — name, version, icon, capabilities, configuration schema — is declared in the manifest.
Component Contexts
WCP 1.3.1 introduced three component roles — "widget", "control",
and "ticker" — because different components are suited to fundamentally different
contexts. Understanding these contexts is what gives the role field its meaning.
The orchestration context
The primary area of a WCP-compatible host — a free-placement surface where instruments are
arranged, resized, and grouped by the user. This is the natural home of "widget"
role components: rich, resizable instruments that the user places deliberately and interacts
with at their leisure. There is no inherent size or behaviour constraint beyond what the host's
layout system supports.
Many host implementations associate a persistent companion area with the orchestration context — a zone that remains visible at all times above or alongside the main instrument area. This companion is the masthead context, described below. Not all hosts implement a masthead, but WCP defines it explicitly because it calls for a distinct class of component behaviour.
The masthead context
A persistent zone — in a typical implementation, a horizontal strip running the full width of the host — that is always visible regardless of what the user is doing in the orchestration area. Two component types can appear here, and they serve different purposes:
- Tickers (
"role":"ticker",mastheadCapable: true) — display-only, scrolling or animated readouts. No interactive controls. A ticker is read, not operated. In a common implementation, tickers occupy the available space between any controls placed in the masthead — an elastic fill that accommodates whatever the host places alongside it. - Masthead controls (
"role":"control",mastheadCapable: true) — interactive components sized to fit within the masthead zone. A control can be as narrow as a single icon or status indicator, or as wide as the design requires — a composite radio station control with selector, play/stop, and volume slider is a single control component. The host and user determine where controls are positioned relative to tickers; the protocol makes no prescription on left, right, or order beyond what the manifest declares.
Sizing note: WCP 1.3.1 expresses masthead height as a range (40–60px) as the initial definition. As experience with the specification grows, additional height profiles are expected to be introduced — allowing instruments to adapt their layout and behaviour to different masthead scales.
Components in the masthead context must be resource-efficient. They are always rendered, never hidden. Heavy computation, frequent polling, or large assets have a constant cost — design accordingly.
The discovery context
Some components are not placed freely by the user — they are administratively authorised for a given installation. The WCP Bonjour service governs this context: an administrator registers which components are available, and the host dashboard reflects that registry. Components in the discovery context have passed an explicit approval step; they are not self-selected by end users.
The Bonjour service itself may take many forms — a dedicated container (for example a Docker or Kubernetes-managed service), a local process, or a remote registry reachable over the network. In all cases, the host connects to it for discovery. The Bonjour service typically knows not only which components are currently running, but also where their images can be obtained if a container is not yet instantiated — enabling it to provision and start a component on demand when authorised by an administrator.
Multiple contexts simultaneously
A component can participate in multiple contexts at once. A weather component might declare
both a "widget" role (a stave instrument showing a detailed forecast) and a
"ticker" role (masthead strip showing current conditions) — both in the same
"components" array, each with its own path and size declaration.
This is why role differentiation exists in the protocol. Without it, a host receiving a
component from a Bonjour registry has no programmatic basis for knowing whether to offer it
as a stave instrument, a masthead ticker, or both. The role field and
mastheadCapable flag are the protocol's answer to that question.
A / B / C Contexts — a Reference Implementation
The Penrith Beacon WCP Dashboard implements the three component contexts as three system-managed orchestrations — Applications, Bonjour, and Controls — which are automatically populated as components are added or discovered. Because every instrument must reside somewhere in the host's structure, there must always be a way for the host to express which components are available, which are authorised, and which are of a specific role. These three orchestrations make those distinctions visible and navigable without requiring any manual organisation by the user.
Applications (A) — the complete inventory
Every component from every source, every role. Applications is the unfiltered view of all WCP presence in the system. Because a host may acquire components from multiple sources — manual addition, Bonjour discovery, future mechanisms not yet defined — there must be one place that holds the complete picture. Applications is that place. Nothing is excluded. This is not a deployment context; it is an inventory.
The name is deliberate. In WCP, every widget is considered an application — a self-contained piece of software with a specific purpose. Some widgets open to fill the entire screen and are indistinguishable from a conventional desktop application. Others remain compact instruments on the stave. The distinction in size does not change what they fundamentally are: purposeful applications. The Applications orchestration reflects this — every widget, regardless of how large or small it renders, belongs here.
Bonjour (B) — the authorised registry
Only "widget" role components that have been registered with the local
WCP Bonjour service. These are the standard widgets an administrator has sanctioned for
this installation. The Bonjour context reflects what the discovery service has authorised —
it is a managed subset of Applications, not a free-for-all. B is always a subset of A.
Controls (C) — the control library
Only "control" role components — the sub-components that applications expose
as individually addressable interactive or display elements. To understand why a dedicated
control library exists, consider what a typical application may contain. A single widget might
expose a handful of controls: a status LED, a play/stop toggle, a volume indicator. A more
complex widget might expose dozens — individual LEDs for separate sensor readings, separate
toggles for separate subsystems, independent status displays for each monitored metric. Every
one of these is a distinct control component in its own right, declared individually in the
manifest and independently deployable.
Without a dedicated library, all of these small, specialised components would sit alongside the full applications in A — making the inventory hard to navigate and obscuring the distinction between a complete application and one of its constituent controls. C exists to gather all controls into one place, giving the host and user a clear, uncluttered surface for assembling control-based arrangements.
C is populated automatically: whenever a new component is added to Applications — whether
it arrived via the Bonjour service, was added manually, or came from any other source — the
host audits it for "control" role entries and places them in C. The application
itself stays in A (and in B if it came via Bonjour); its controls are additionally surfaced
in C. A user assembling a Control Panel or masthead arrangement works entirely from C — every
control from every application in the system is accessible there, without needing to know
which application it came from. C is always a subset of A.
System vs user orchestrations
The key protocol distinction expressed by A, B, and C is between system-managed orchestrations and user-managed orchestrations. System orchestrations are populated automatically by the host from the protocol's role and source distinctions. They are not editable directly — components flow into them as a consequence of how they were added or discovered, not as the result of a manual arrangement. User orchestrations, by contrast, are created and curated entirely by the user: they define the purposeful groupings that give the dashboard its meaning. A user orchestration is where instruments are actually deployed and arranged for a specific purpose.
How a host visually distinguishes system orchestrations from user orchestrations is an implementation choice — whether by position, colour, icon, label, or any other signal. The protocol simply guarantees that the distinction exists and that system orchestrations are populated automatically.
Instrument deployment and the transformer pattern
Each component in A, B, or C is represented as a 1×1 icon — generated by the host from
the component's name and icon. This icon is not the deployed
instrument; it is a representation of the component's availability. Deployment
is a deliberate action — the user selects a component from a system orchestration and adds
it to a user orchestration.
When a component is deployed into a user orchestration, the icon transforms
into the full instrument at its declared defaultSize:
- Single-component server — the component instantiates immediately at
its declared
defaultSize. No further interaction required. - Multi-component server — a picker appears: "This server contains
multiple components. Which would you like to add here?" The user selects one
component from the list. That component instantiates at its
defaultSize.
The component in the system orchestration remains available after deployment — adding an instrument to a user orchestration does not remove it from Applications, Bonjour, or Controls. Each user orchestration holds its own independently-positioned instance.
"components" array. Without structured role
differentiation in the manifest, the host has no information on which to base the choice.
The protocol makes this interaction possible — the host makes it accessible.
Initial state
All three system orchestrations exist from first launch but start empty. Applications and Controls populate as the user adds instruments and control components to the system. Bonjour populates when a WCP Bonjour service is connected and begins reporting authorised components. A host should communicate to the user how each system orchestration is populated, particularly when one is empty.
Mandatory Endpoints
A WCP instrument is, at its core, an HTTP server. In the most common deployment pattern it runs inside a container — a Docker or Kubernetes-managed service that exposes a port on the local network. The host dashboard connects to that port by URL. This is why WCP instruments are defined by HTTP endpoints rather than by library imports or binary protocols: any process that can serve HTTP and respond to the correct paths is a valid WCP instrument, regardless of the language, framework, or infrastructure it runs on.
Every WCP-compliant widget server MUST expose these four endpoints under the path prefix /widget/:
| Endpoint | Method | Description |
|---|---|---|
| /widget/ | GET | Compact widget HTML — loaded by the dashboard in an iframe |
| /widget/wcp | GET | WCP manifest (JSON) — the widget's self-description |
| /widget/health | GET | Health check — returns {"status":"ok","name":"<name>"} |
| /widget/icon.svg | GET | Widget icon (SVG preferred, PNG fallback) |
Access-Control-Allow-Origin: * and handle OPTIONS preflight requests with 204 No Content.Response Contracts
| Endpoint | HTTP status | Content-Type | Response body |
|---|---|---|---|
| GET /widget/ | 200 | text/html | Complete HTML document — must include <!DOCTYPE html>, <meta charset>, and <meta name="viewport">. Not a fragment. |
| GET /widget/wcp | 200 | application/json | WCP manifest object — see Manifest Schema. |
| GET /widget/health | 200 (healthy) 503 (unavailable) |
application/json |
Healthy: {"status":"ok","name":"My Widget"}Unhealthy: {"status":"error","name":"My Widget","message":"Database unreachable"}
|
| GET /widget/icon.svg | 200 | image/svg+xml or image/png | SVG: vector, any declared dimensions — scales cleanly to any host size. PNG fallback: minimum 256×256 px. |
Optional Endpoints
| Endpoint | Method | Description |
|---|---|---|
| Manifest | ||
| /widget/manifest | GET | Deprecated — WCP 1.3.0. Pre-WCP host compatibility shim. New implementations must not implement this. Hosts must not call it. |
| Configuration | ||
| /widget/configure | POST | Accept configuration JSON from the host. Request: Content-Type: application/json, body is a flat object {"fieldId":"value",...}. Response: {"success":true} on 200, {"success":false,"error":"reason"} on 400. See Widget Configuration. WCP 1.1.0 |
| /widget/api/search | GET | Server-backed autocomplete for autocomplete config fields. Params: ?q=<query> (required), &field=<fieldId> (present when manifest has multiple autocomplete fields). Response: application/json array of strings. See Widget Configuration. WCP 1.1.0 |
| Screen-size views | ||
| /widget/full | GET | Full-page desktop view — complete HTML document, opened in a new window or tab by the host |
| /widget/mobile-portrait | GET | Mobile portrait view — complete HTML document, 4-column / 60px-row grid WCP 1.2.0 |
| /widget/mobile-landscape | GET | Mobile landscape view — complete HTML document, 8-column / 60px-row grid WCP 1.2.0 |
| Masthead | ||
| /widget/ticker | GET | Masthead ticker view — complete HTML document, thin horizontal strip, display-only WCP 1.3.0 |
| /widget/control/<id> | GET | Individual control component view — complete HTML document. <id> is the id field of the component entry in the manifest's components array (e.g. /widget/control/temp-led). WCP 1.3.0 |
| API | ||
| Export | ||
| /widget/export.wcp | GET | Export this container as a .wcp package (zip: manifest + icon + docs) WCP 1.3.0 |
| /widget/api/guids | GET | Return the server UUID and all component UUIDs — used by Bonjour for GUID-based discovery WCP 1.3.0 |
| API | ||
| /widget/api/* | any | Widget-specific API routes |
/widget/full in a native utility
window. When that window uses titleBarStyle: hiddenInset (which blends the
title bar into the page area), 100vh is slightly larger than the visible area
due to the title bar overlap. Always use height: 100% cascaded from
html, body { height: 100% } in full-page layouts — this works correctly across
all host environments including Electron, standard browsers, and other Chromium embeddings.
WCP Manifest Schema
The WCP manifest is the self-description of a widget server. It is the single document a host needs to discover everything about a server — its name, version, what components it exposes, what pages it can open, what actions it supports, and how it should be configured. The host fetches it once at registration time and caches it; the manifest is the contract between server and host.
Every WCP server must serve the manifest at GET /widget/wcp. The host uses
it to build the widget card, populate the Add Widget picker, construct the configuration
form, and determine which system orchestrations the server's components belong in.
The manifest below shows the full schema. All top-level fields except
wcp, uuid, name, version,
description, icon, health, and
components are optional — include only what your server supports.
GET /widget/wcp returns:
{
// ── Server identity ──────────────────────────────────────────
"wcp": "1.4.0",
"uuid": "31b26fb7-5dd7-4b05-a114-6e36040b33f1",
"name": "My Widget",
"version": "1.0.0",
"description":"What it does.",
"icon": "/widget/icon.svg",
"health": "/widget/health",
// ── Components ───────────────────────────────────────────────
"components": [
{
"id": "main",
"uuid": "911fcf76-f2f3-478a-8897-c42b47e87de3",
"name": "My Widget",
"role": "widget", // "widget" | "control" | "ticker"
"path": "/widget/",
"icon": "/widget/icon.svg",
"renderMode": "iframe",
"defaultSize": { "w": 4, "h": 3 }
}
],
// ── Pages ────────────────────────────────────────────────────
"pages": [{
"id": "full",
"path": "/widget/full",
"title": "My Widget — Full Page",
"window": { "width": 1100, "height": 700 }
}],
// ── Actions ───────────────────────────────────────────────────
"actions": [
{ "id":"open-window", "type":"wcp:open-window", "label":"Open Full Screen", "page":"full" },
{ "id":"open-tab", "type":"wcp:open-tab", "label":"Open in Tab", "page":"full",
"tab":{ "title":"My Widget", "icon":"/widget/icon.svg" }, "persist":false }
],
// ── Configuration (WCP 1.1.0) ────────────────────────────────
"config": [ /* see Widget Configuration */ ],
// ── Adaptive views (WCP 1.2.0) ───────────────────────────────
"views": {
"desktop": "/widget/full",
"mobile-portrait": "/widget/mobile-portrait",
"mobile-landscape": "/widget/mobile-landscape"
}
}
Field Reference
| Field | Required | Description |
|---|---|---|
| wcp | required | Protocol version — currently "1.4.0" |
| uuid | required | Permanent UUID v4 for this server — identifies the server as a whole, independently of its components. Must be distinct from every component UUID declared in this server's components array. Once assigned, never changes. This is the UUID that appears in the widgets[].uuid field of a Container Directory entry. WCP 1.4.0 |
| name | required | Human-readable widget name |
| version | required | Widget version (semver) |
| description | required | One or two sentences |
| icon | required | Path to icon (relative to widget origin) |
| health | required | Path to health endpoint |
| components | required | Array of component descriptors — declares all widgets, controls, and tickers this server exposes. See Components Array. |
| pages | optional | Full-page views the host can open in a new window or tab |
| actions | optional | Action buttons shown on the dashboard card |
| config | optional | Configuration schema — fields the host renders as a settings form (WCP 1.1.0) |
| views.desktop | optional | Path to full desktop view (WCP 1.2.0) |
| views.mobile-portrait | optional | Path to mobile portrait view — 4-col/60px (WCP 1.2.0) |
| views.mobile-landscape | optional | Path to mobile landscape view — 8-col/60px (WCP 1.2.0) |
Components Array WCP 1.3.0
The "components" array is a required field inside the WCP manifest
— the same document returned by GET /widget/wcp. There is no separate endpoint.
It sits alongside "name", "pages", "actions",
and the other top-level manifest fields. Every WCP 1.3.0 server must declare at least one
component.
A WCP server exposes a collection of one or more components. Each component is an independently addressable widget, control, or ticker. The number of entries determines host behaviour:
- One entry — the host uses that component directly. No picker is shown. A single-component server is the common case for most widgets.
- Two or more entries — the host presents a picker when the user adds the server, allowing selection of which component to deploy.
"components" is mandatory in
WCP 1.3.0, every developer reading any WCP manifest knows exactly where to look —
regardless of how many components a server exposes. One structure, always.
"components"
array is required in every WCP 1.3.0+ manifest. Servers still using the
flat "widget", "ticker", or "control" top-level
objects from WCP 1.0.0–1.2.0 are not compliant with WCP 1.3.0 or later and must be updated.
Server-level vs component-level fields
The manifest has two levels. Server-level fields describe the server as a whole —
they always sit at the top level. Component-level fields describe individual components
and always sit inside each entry in the "components" array.
| Level | Fields |
|---|---|
| Server level (top of manifest) | "wcp", "uuid", "name", "version", "description", "icon", "health", "components", "pages", "actions", "config", "views" |
Component level (inside each "components" entry) | "id", "uuid", "name", "role", "path", "icon", "renderMode", "defaultSize", "mastheadCapable", "masthead" |
Complete manifest example
A full WCP 1.4.0 manifest — nothing omitted:
{
// ── Server identity ──────────────────────────────────────────
"wcp": "1.4.0",
"uuid": "4c548b34-778b-4d0b-955c-6b083b993e59",
"name": "IoT Monitor Suite",
"version": "1.0.0",
"description": "IoT monitoring dashboard, status LEDs, and a live data ticker.",
"icon": "/widget/icon.svg",
"health": "/widget/health",
// ── Server-level pages (unchanged) ───────────────────────────
"pages": [
{
"id": "full",
"path": "/widget/full",
"title": "IoT Monitor — Full View",
"description": "Full-page dashboard for all IoT sensors.",
"window": { "width": 1100, "height": 700 }
}
],
// ── Server-level actions (unchanged) ─────────────────────────
"actions": [
{
"id": "open-window",
"type": "wcp:open-window",
"label": "Open Full View",
"page": "full"
},
{
"id": "open-tab",
"type": "wcp:open-tab",
"label": "Open in New Tab",
"page": "full",
"tab": { "title": "IoT Monitor", "icon": "/widget/icon.svg" },
"persist": false
}
],
// ── Server-level adaptive views (unchanged) ──────────────────
"views": {
"desktop": "/widget/full",
"mobile-portrait": "/widget/mobile-portrait"
},
// ── Components ───────────────────────────────────────────────
"components": [
{
"id": "dashboard",
"uuid": "acc082d5-3e69-4dd5-a233-bd4be5b2675a",
"name": "IoT Dashboard",
"role": "widget", // "widget" | "control" | "ticker"
"path": "/widget/",
"icon": "/widget/icon.svg",
"renderMode": "iframe",
"defaultSize": { "w": 6, "h": 4 }
},
{
"id": "temp-led",
"uuid": "9f8bded4-2bd9-45e0-bda7-d1989f516e5c",
"name": "Temperature LED",
"role": "control",
"path": "/widget/control/temperature",
"icon": "/widget/icon.svg",
"mastheadCapable": false,
"size": { "min": 40, "max": 60 }
},
{
"id": "status-ticker",
"uuid": "a13ead5f-f5d5-4ff4-a458-75622824c287",
"name": "Status Ticker",
"role": "ticker",
"path": "/widget/ticker",
"icon": "/widget/icon.svg",
"mastheadCapable": true,
"masthead": {
"height": { "min": 40, "max": 60 },
"width": { "min": 160, "max": 240 }
}
}
]
}
Component field reference
| Field | Required | Description |
|---|---|---|
| id | required | Unique identifier within this server's component list |
| name | required | Human-readable display name — used for 1×1 icon label and picker |
| role | required | "widget" | "control" | "ticker" |
| path | required | URL path to this component's HTML view |
| icon | optional | Icon path for this component; falls back to server-level icon |
| renderMode | optional | "iframe" (default) — applies to "widget" role |
| defaultSize | optional | Grid size {w,h} — applies to "widget" role |
| mastheadCapable | optional | true if this component can appear in the masthead — applies to "control" and "ticker" roles |
| uuid | required | Permanent UUID v4 for this component — used for GUID-based discovery (Bonjour) and orchestration theme references. Once assigned, never changes. Must be distinct from the server-level uuid field and from every other component UUID in this server. Generate with any standard UUID v4 tool and commit it to the codebase; never regenerate. |
| masthead | optional | Masthead rendering dimensions — see below. Required when mastheadCapable: true. |
| masthead.height | optional | {"min":40,"max":60} — height range in px. Host renders at masthead bar height, clamped to this range. |
| masthead.width | optional | {"min":160,"max":240} — width range in px for "control" role. Absent on tickers — they fill remaining space elastically. |
Auto-distribution on import
When a WCP Bonjour service imports a server, each component is distributed to the appropriate system orchestration automatically:
| Component role | System orchestration | Also in |
|---|---|---|
"widget" | Bonjour (B) | Applications (A) |
"control" | Controls (C) | Applications (A) |
"control" with mastheadCapable: true + masthead object | Controls (C) + Masthead Controls settings | Applications (A) |
"ticker" with mastheadCapable: true + masthead object | Masthead Tickers settings | Applications (A) |
Each component gets its own 1×1 icon card generated from its name + icon.
The component remains independently draggable and transformable.
postMessage Protocol
Widgets inside iframes communicate with the host via window.parent.postMessage(payload, '*').
| type | Required fields | Host action |
|---|---|---|
| wcp:open-window | page, width?, height? | Opens named page in a new native window |
| wcp:open-tab | page, tab:{title,icon} | Opens page as a persistent dashboard tab |
| wcp:copy-to-clipboard | text | Copies text — bypasses iframe clipboard sandbox |
| wcp:download-file | filename, content, mimeType | Triggers file save dialog |
| wcp:import-theme | url (to a .pbtheme.json) | Fetches and imports a dashboard theme |
Example
// Open full-page view
window.parent.postMessage({ type: 'wcp:open-window', page: 'full', width: 1100, height: 700 }, '*');
// Copy text (clipboard blocked in sandboxed iframes)
window.parent.postMessage({ type: 'wcp:copy-to-clipboard', text: 'Hello!' }, '*');
wcp:copy-to-clipboard and wcp:import-theme are exempt from this check.Widget Configuration WCP 1.1.0
How configuration works end to end
When a user adds a widget manually, they provide the dashboard with a URL — the network address of a running widget container. The dashboard immediately fetches the widget's manifest:
GET http://192.168.1.42:3739/widget/wcp
That single request returns the complete manifest JSON — the widget's name, version,
components, pages, actions, and crucially, its config array. The dashboard
reads the config array and dynamically builds a settings form
without any further communication with the widget. No additional endpoint is needed; the
manifest is the complete specification of what the form should look like.
For each entry in config, the dashboard renders a different UI control
based on the type field:
autocomplete— a text input. As the user types, the dashboard queries the widget's search endpoint live and shows matching suggestions below the input.select— a dropdown. The options come directly from theoptionsarray in the config entry; no query to the widget is needed.number— a numeric input. Theminandmaxvalues in the config entry become the input's bounds.text— a free-text input.
Taking the weather widget as a concrete illustration: its manifest contains three
config entries. The dashboard reads them and presents the user with exactly
three controls — a live-search location field, a Celsius/Fahrenheit dropdown, and a number
input bounded to 5–60. The user did not install a form. The widget did not serve one.
The dashboard built it entirely from the JSON it received at GET /widget/wcp.
Once the user interacts with an autocomplete field — typing a location,
a product name, or any other search term — the live search mechanism activates. That is
where the search proxy and /widget/api/search come in, as described below.
When the user has completed all fields and saves, the dashboard POSTs the collected values
to /widget/configure, including a Wcp-Instance-Id header that
uniquely identifies this widget placement. The widget server stores the configuration
under that ID, responds with success, and the dashboard then fetches the widget HTML
using the same instance ID — the server injects the configuration into the HTML so the
widget JavaScript has it immediately on load.
Config Field Types
| type | Description | Required extra fields |
|---|---|---|
| autocomplete | Server-backed live search. The host proxies the user's typed query to the widget's /widget/api/search endpoint and displays the returned strings as selectable suggestions. The widget decides what to search and what to return — cities, products, hosts, employees, or anything else. See contract below. | searchUrl, placeholder |
| select | Dropdown with a fixed list of options declared in the manifest. | options: [{value, label}] |
| number | Numeric input with optional bounds. | min, max, step |
| text | Free-text input. | placeholder, maxLength |
| password | Free-text input rendered as a masked/password field in the host UI. Implies sensitive: true automatically. | placeholder |
Optional flag: sensitive
Any configuration field may include "sensitive": true. This signals to the
dashboard that the value is sensitive credential material (API tokens, account IDs, OAuth
secrets, encryption keys, etc.). Dashboards MUST obey the following rules
for any field marked sensitive, or any field of type password (which implies
sensitive):
- MUST NOT persist the value in any export artefact —
.wcpo,.wcpx, or any other dashboard-level export format. - MUST NOT include the value in any log, telemetry, or analytics payload.
- SHOULD mask the value in any UI that re-displays it after entry (showing only the last few characters, or an obscured placeholder).
- MAY store the value in dashboard-local secure storage (e.g. the OS keychain) as long as exports omit it.
Once the dashboard POSTs the value to the widget's /widget/configure endpoint,
the widget owns the value and is responsible for storing it appropriately — typically keyed
by Wcp-Instance-Id.
sensitive flag makes the
rule machine-checkable: a dashboard that strips sensitive fields by inspecting the manifest
cannot accidentally export a token even if the widget developer forgot to flag it via
type: "password".
POST /widget/configure — contract
When the user saves the configuration form, the host assembles all field values into a
flat JSON object keyed by field id, and POSTs it to the widget:
// Request
POST /widget/configure
Content-Type: application/json
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
Wcp-Dashboard-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Wcp-Version: 1.3.1
{ "fieldId": "selectedValue", "fieldId2": 42, "fieldId3": "anotherValue" }
// Success response
HTTP 200
Content-Type: application/json
{ "success": true }
// Error response
HTTP 400
Content-Type: application/json
{ "success": false, "error": "Location not found" }
The widget server stores the configuration keyed by Wcp-Instance-Id. This
allows a single container to hold independent configurations for multiple dashboard instances
simultaneously. See WCP Request Headers for the full instance
lifecycle.
On success the dashboard proceeds to request the widget HTML. On error the host may
display the error message to the user.
What happens after a successful configure
Once the widget responds {"success": true}, the dashboard loads the widget
into an iframe by calling the component's declared path — typically
GET /widget/ — with the same Wcp-Instance-Id header:
GET http://192.168.1.42:3739/widget/
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
The widget server looks up the stored configuration for that instance ID and injects it into the HTML before serving. The widget's JavaScript then has the configuration available immediately on load and uses it for all subsequent API calls — passing parameters inline rather than asking the server to look them up again. The server may retain or discard the stored configuration after this point; both are valid. See WCP Request Headers — Multi-instance server model for patterns A and B.
GET /widget/api/search — contract
Implemented by widgets that declare one or more autocomplete config fields.
The host calls this endpoint — via its server-side proxy — each time the user types in
an autocomplete field.
// Request — single autocomplete field
GET /widget/api/search?q=Par
// Request — multiple autocomplete fields pointing to the same searchUrl
// The host adds &field=<fieldId> so the widget knows which dataset to search
GET /widget/api/search?q=john&field=salesRep
// Response (both cases)
HTTP 200
Content-Type: application/json
["Result one", "Result two", "Result three"]
// Empty result
HTTP 200
Content-Type: application/json
[]
| Parameter | Required | Description |
|---|---|---|
| q | required | The user's current typed input. May be empty string on initial focus. |
| field | optional | The id of the config field making this request. Present when the manifest declares two or more autocomplete fields with the same searchUrl. Use this to branch search logic on the server. |
The response is always a JSON array of plain strings. The host displays each string as a
selectable suggestion. The string the user selects is stored verbatim and included in the
POST /widget/configure body when the form is saved.
Why the proxy exists
The host's config form is served from the dashboard's own origin. Widget servers run as containers elsewhere on the local network — discovered via the Bonjour service, which knows the host address and port of every registered widget. That network address is a different origin from the dashboard, so a direct browser call to the widget would be a cross-origin request subject to CORS enforcement. WCP routes it through the dashboard's own server instead:
// Browser calls the dashboard — same origin, no CORS:
GET /api/widget-search?url=http://192.168.1.42:3739&q=Par
// Dashboard server calls the widget on the local network — server-to-server, no CORS:
GET http://192.168.1.42:3739/widget/api/search?q=Par
// Note: 192.168.1.42:3739 is illustrative — the actual address and port of a widget
// container on the local network is resolved by the Bonjour service at registration time.
The url parameter identifies which widget to query. One proxy endpoint
handles all widgets regardless of where they are hosted. Widget developers implement only
/widget/api/search — the host handles the proxy transparently.
Example 1 — Weather widget: location, units, refresh
Use case: A weather monitoring widget. The user selects a location by typing and choosing from live suggestions, picks a temperature unit, and sets a refresh interval. The location list is dynamic — queried live from the widget's server at each keypress.
// In the manifest "config" array:
"config": [
{
"id": "location",
"type": "autocomplete",
"label": "Location",
"placeholder": "Search for a city or region…",
"searchUrl": "http://192.168.1.42:3739" // address resolved by Bonjour at registration
},
{
"id": "units",
"type": "select",
"label": "Temperature units",
"options": [
{ "value": "celsius", "label": "°C — Celsius" },
{ "value": "fahrenheit", "label": "°F — Fahrenheit" }
]
},
{
"id": "refresh",
"type": "number",
"label": "Refresh interval (minutes)",
"min": 5,
"max": 60
}
]
// User types "Par" — host proxies to widget:
GET http://192.168.1.42:3739/widget/api/search?q=Par
// Widget responds:
["Paris, France", "Paris, Texas, USA", "Parramatta, NSW, Australia", "Parma, Italy"]
// User selects "Paris, France", picks Celsius, enters 15. Host POSTs:
POST /widget/configure
{ "location": "Paris, France", "units": "celsius", "refresh": 15 }
// Widget responds:
{ "success": true }
Example 2 — Inventory widget: product search, currency, alert threshold
Use case: An inventory management widget showing live stock levels for a selected product. Products are searched from the widget's own database — there may be thousands of SKUs, making a fixed dropdown impractical.
"config": [
{
"id": "product",
"type": "autocomplete",
"label": "Product",
"placeholder": "Search by product name or SKU…",
"searchUrl": "http://192.168.1.43:3741"
},
{
"id": "currency",
"type": "select",
"label": "Display currency",
"options": [
{ "value": "USD", "label": "US Dollar" },
{ "value": "EUR", "label": "Euro" },
{ "value": "GBP", "label": "British Pound" }
]
},
{
"id": "lowStockThreshold",
"type": "number",
"label": "Low-stock alert (units remaining)",
"min": 0,
"max": 10000
}
]
GET http://192.168.1.43:3741/widget/api/search?q=widget
→ ["Widget Type A (SKU-1042)", "Widget Pro (SKU-1087)", "Micro Widget (SKU-2201)"]
POST /widget/configure
{ "product": "Widget Pro (SKU-1087)", "currency": "USD", "lowStockThreshold": 50 }
Example 3 — Infrastructure monitoring: host search, metric, alert
Use case: A monitoring widget displaying CPU, memory, and health metrics for a selected server or container. The available hosts are queried live from the widget's infrastructure registry — containers may appear and disappear as deployments change.
"config": [
{
"id": "target",
"type": "autocomplete",
"label": "Host or Container",
"placeholder": "Search for a server or container…",
"searchUrl": "http://192.168.1.44:3742"
},
{
"id": "metric",
"type": "select",
"label": "Primary metric",
"options": [
{ "value": "cpu", "label": "CPU usage %" },
{ "value": "memory", "label": "Memory usage %" },
{ "value": "disk", "label": "Disk I/O" },
{ "value": "network", "label": "Network throughput" }
]
},
{
"id": "alertThreshold",
"type": "number",
"label": "Alert threshold (%)",
"min": 1,
"max": 100
}
]
GET http://192.168.1.44:3742/widget/api/search?q=web
→ ["web-server-01 (192.168.1.10)", "web-server-02 (192.168.1.11)", "web-proxy (192.168.1.5)"]
POST /widget/configure
{ "target": "web-server-01 (192.168.1.10)", "metric": "cpu", "alertThreshold": 85 }
Example 4 — Sales pipeline: compound autocomplete (three live searches)
Use case: A sales pipeline widget showing filtered deals. The user
must configure three separately searched entities — a sales representative, a product
category, and a customer account — plus a fixed reporting period. All three autocomplete
fields share the same searchUrl but search different datasets. The host
passes &field=<fieldId> so the widget can branch its search logic.
"config": [
{
"id": "salesRep",
"type": "autocomplete",
"label": "Sales Representative",
"placeholder": "Search for a sales rep…",
"searchUrl": "http://192.168.1.45:3743"
},
{
"id": "productCategory",
"type": "autocomplete",
"label": "Product Category",
"placeholder": "Search product categories…",
"searchUrl": "http://192.168.1.45:3743"
},
{
"id": "account",
"type": "autocomplete",
"label": "Customer Account",
"placeholder": "Search for a customer…",
"searchUrl": "http://192.168.1.45:3743"
},
{
"id": "period",
"type": "select",
"label": "Reporting Period",
"options": [
{ "value": "thisMonth", "label": "This month" },
{ "value": "thisQuarter", "label": "This quarter" },
{ "value": "thisYear", "label": "This year" }
]
}
]
// User types "john" in the Sales Rep field — host passes &field=salesRep:
GET http://192.168.1.45:3743/widget/api/search?q=john&field=salesRep
→ ["John Adams (East Region)", "John Clarke (West Region)"]
// User types "ent" in Product Category — host passes &field=productCategory:
GET http://192.168.1.45:3743/widget/api/search?q=ent&field=productCategory
→ ["Enterprise Software", "Entertainment", "Environmental Services"]
// Widget server branches on the field parameter:
app.get('/widget/api/search', (req, res) => {
const { q, field } = req.query;
switch (field) {
case 'salesRep': return res.json(searchStaff(q));
case 'productCategory': return res.json(searchCategories(q));
case 'account': return res.json(searchCustomers(q));
default: return res.json([]);
}
});
// After all fields are filled, host POSTs:
POST /widget/configure
{
"salesRep": "John Adams (East Region)",
"productCategory": "Enterprise Software",
"account": "Acme Corporation",
"period": "thisQuarter"
}
// Widget displays: deals by John Adams / Enterprise Software / Acme Corp / this quarter.
CORS & Security
Required CORS Headers
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Wcp-Instance-Id, Wcp-Dashboard-Id, Wcp-Version, Wcp-Widget-Id, Wcp-Orchestration-Id, Wcp-Application-Id
Return 204 No Content for OPTIONS preflight requests. Failing to handle OPTIONS causes cross-origin POST requests to /widget/configure to fail silently. All Wcp-* headers must be listed in Access-Control-Allow-Headers or the browser will block requests that include them. Wcp-Widget-Id was added in WCP 1.4.0 and Wcp-Orchestration-Id / Wcp-Application-Id in WCP 1.5.0 — add all three even if you do not yet implement their corresponding features. This is a forward-compatibility measure that costs nothing and prevents silent request failures when a newer host connects.
Iframe Sandbox
Widgets run in a sandboxed <iframe>. navigator.clipboard, file downloads, and window.open are blocked. Use the postMessage protocol — the host acts on the widget's behalf.
Origin Validation
Hosts should reject postMessage events from unknown origins, storing the widget origin at registration time. wcp:copy-to-clipboard and wcp:import-theme may be exempted.
WCP Request Headers
WCP defines four request headers that coordinate communication between the dashboard and
widget servers. All four follow the Wcp- namespace convention — no X-
prefix, in accordance with RFC 6648.
| Header | Required | Description |
|---|---|---|
| Wcp-Instance-Id | required | A UUID generated by the dashboard when a widget is placed on a stave. Identifies this specific instance of the widget — distinct from every other placement of the same widget, on this dashboard or any other. Present on POST /widget/configure and GET /widget/. The widget server keys stored configurations by this value so that multiple independent instances can be served from a single container simultaneously. |
| Wcp-Dashboard-Id | optional | A UUID identifying the dashboard making the request. Allows widget servers to distinguish which host is communicating — useful for logging, analytics, or access control. |
| Wcp-Version | optional | The WCP protocol version the dashboard is speaking — e.g. 1.4.0. Allows widget servers to negotiate behaviour or reject incompatible hosts in future protocol versions. |
| Wcp-Widget-Id | optional WCP 1.4.0 | The id of the widget selected from a Container Directory. Sent on all requests after the user selects a widget from a multi-widget container — manifest fetch, configure, iframe load, and all subsequent API calls. Allows multi-widget containers to identify which widget is being addressed at the HTTP layer, independently of URL path routing. Not sent when the host falls back to the legacy single-widget flow (i.e. when GET /wcp returned 404). |
| Wcp-Orchestration-Id | optional WCP 1.5.0 | The UUID of the orchestration currently displayed by the host. Sent on all widget
requests — iframe loads, configure, and proxied API calls. Components from the same
orchestration share this value, allowing widget servers to use it as a shared state key
for intra-orchestration coordination (e.g. a player widget and its companion LED or
ticker components). Also passed as the query parameter wcpOrchestrationId
on iframe src URLs, because browsers cannot set custom headers on iframe loads. |
| Wcp-Application-Id | optional WCP 1.5.0 | A UUID generated by the host once per application window at launch time. Present
only when an orchestration is running as a launched standalone application, not when
viewed in the design tool. Its presence lets widget servers distinguish an application
window from the design tool even when both display the same orchestration. Also passed
as the query parameter wcpApplicationId on iframe src URLs. |
Instance lifecycle
The dashboard generates a Wcp-Instance-Id UUID the first time a widget is
placed on a stave. That UUID is stored alongside the widget in the stave data and remains
stable for the lifetime of that placement. It is sent on every request to that widget —
configure, serve, and any server-side proxy calls the dashboard makes on the widget's behalf.
.wcpo file, Wcp-Instance-Id values are stripped
from all instruments. When an orchestration is imported, the receiving dashboard generates
fresh UUIDs for every widget. An instance ID is meaningful only to the dashboard that
created it and the widget server it is talking to.
Multi-instance server model
Before WCP 1.3.1, a widget container implicitly served a single global configuration.
With Wcp-Instance-Id, a single container can serve many independent instances
simultaneously — each with its own configuration. Two dashboards can each place a weather
widget from the same container and configure it for different cities; the container
distinguishes them by instance ID.
The flow for a configurable widget:
- Dashboard POSTs configuration with
Wcp-Instance-Id: <uuid>→ widget server stores config keyed by that UUID - Dashboard GETs
/widget/with the sameWcp-Instance-Id→ widget server retrieves the stored config and injects it into the HTML it serves, typically as a JavaScript variable:<script>const WCP_INSTANCE_ID = "b4f3a1c2-…"; const WCP_CONFIG = { "location": "Paris, France", "units": "celsius" };</script> - Widget JavaScript uses
WCP_CONFIGdirectly for all subsequent API calls, passing parameters inline — the server does not need to look up the configuration again:fetch(`/widget/api/weather?location=${WCP_CONFIG.location}&units=${WCP_CONFIG.units}`);
The widget server may retain the stored configuration for re-serve optimisation (e.g. if the dashboard reloads and GETs the widget again), or may discard it after the initial serve. Both are valid. Widget developers should document their chosen behaviour.
Context-scoped runtime state WCP 1.5.0
Wcp-Instance-Id is designed for configuration isolation — each
widget placement has its own stored config. But many widgets also maintain runtime
state (playback position, live status, session data) that must be shared across
components of the same widget co-located in the same orchestration, while remaining
isolated from the same widget in a different orchestration or application window.
Use Wcp-Orchestration-Id and Wcp-Application-Id together as
a compound state key. All components in the same orchestration share the same key;
different orchestrations get different keys; a launched application window gets its own
key even when it shows the same orchestration as the design tool.
| Context | Wcp-Orchestration-Id | Wcp-Application-Id | Effective state key |
|---|---|---|---|
| Design tool showing Orchestration A | orch-A | — | orch-A |
| Design tool showing Orchestration B | orch-B | — | orch-B |
| Application window for Orchestration A | orch-A | app-1 |
orch-A:app-1 |
| Second application window, same orchestration | orch-A | app-2 |
orch-A:app-2 |
| Design tool and application window, same orchestration | orch-A / orch-A |
— / app-1 |
orch-A vs orch-A:app-1 — isolated ✓ |
The widget server derives the key with this logic (shown as pseudocode — any language or framework may implement it equivalently):
FUNCTION get_state_key(request):
orch_id ← request.header("Wcp-Orchestration-Id") OR ""
app_id ← request.header("Wcp-Application-Id") OR ""
IF orch_id AND app_id:
RETURN orch_id + ":" + app_id
IF orch_id:
RETURN orch_id
RETURN "global" // fallback — host pre-dates WCP 1.5.0
Widget JavaScript uses the values injected into the page at serve time (as
WCP_ORCHESTRATION_ID and WCP_APPLICATION_ID constants) when
making same-origin API calls, forwarding them as request headers. This keeps the
server-side key stable across every state read and write from every component in the
orchestration:
// Constants injected by the server at serve time
const WCP_ORCHESTRATION_ID = "orch-A";
const WCP_APPLICATION_ID = ""; // empty in design tool; set in application windows
// Helper used for all same-origin state API calls
function wcpFetch(url, opts) {
opts = opts || {};
const extra = {};
if (WCP_ORCHESTRATION_ID) extra['Wcp-Orchestration-Id'] = WCP_ORCHESTRATION_ID;
if (WCP_APPLICATION_ID) extra['Wcp-Application-Id'] = WCP_APPLICATION_ID;
opts.headers = Object.assign({}, opts.headers || {}, extra);
return fetch(url, opts);
}
wcpFetch('/widget/api/state', { method: 'POST', ... });
Security layering
The WCP headers operate at the protocol coordination layer. Security is orthogonal and
sits on top. An open widget server requires only Wcp-Instance-Id to support
multi-instance operation. A secured widget server — where an administrator has applied an
authentication requirement — adds standard HTTP security headers alongside the WCP headers:
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
Wcp-Dashboard-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Wcp-Version: 1.3.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
The instance identification mechanism is unchanged regardless of security posture.
The widget server validates the bearer token first; if valid, it proceeds to use
Wcp-Instance-Id to locate the correct configuration.
Container Directory WCP 1.4.0
Prior to WCP 1.4.0, a container was assumed to serve a single widget. The host resolved a
widget by calling GET /widget/wcp directly, and there was no protocol mechanism
to express that a single container might host more than one independently-selectable widget.
The Container Directory specification lifts that constraint.
A container that implements the directory endpoint advertises itself as a multi-widget host.
Hosts that support WCP 1.4.0 query GET /wcp before falling back to
GET /widget/wcp — allowing them to display a picker when a container offers
multiple widgets, while remaining fully compatible with all existing single-widget containers
that return 404 for the directory endpoint.
The directory endpoint: GET /wcp
The directory endpoint is located at the container root, not under the
/widget/ namespace. This is intentional: the directory describes the container,
not any individual widget within it. A container that serves three widgets has one directory
endpoint and three widget endpoint sets — they operate at different levels of abstraction.
GET http://192.168.1.42:3744/wcp
↳ Returns the container directory listing all hosted widgets
The response is a JSON object with "type": "directory" to distinguish it
unambiguously from a WCP manifest (which has "wcp" as a version string). Every
entry in the "widgets" array identifies one hosted widget and declares the path
to its manifest, so the host knows exactly where to issue the next call.
Directory response schema
| Field | Required | Type | Description |
|---|---|---|---|
| type | required | string | Always "directory". Allows the host to distinguish this response from a
WCP manifest in the event that GET /wcp is inadvertently mapped to a manifest
endpoint on a non-compliant server. |
| wcp | required | string | The WCP version this directory response conforms to — e.g. "1.4.0". Allows
hosts to detect whether the container supports directory features introduced in later versions. |
| widgets | required | array | Ordered list of widget entries. The ordering is the container author's recommended presentation order — hosts may present them in a different order but should default to the declared order. |
| widgets[].id | required | string | A stable, unique identifier for this widget within the container. Kebab-case recommended
(e.g. "github", "cloudflare-dns"). Sent as the
Wcp-Widget-Id header on all subsequent requests once the user has selected this
widget. Must not change across container restarts or version upgrades — it is stored by the
host alongside the instance ID. |
| widgets[].uuid | required | string (UUID v4) | The server-level uuid from the manifest returned at
widgets[].manifest — the UUID that identifies this widget server as a whole,
not any individual component within it. Allows Bonjour to identify and register a server
at the directory level without fetching each manifest individually. Must match the
top-level uuid field in the manifest exactly. Once assigned, never changes. |
| widgets[].name | required | string | Human-readable display name shown in the host's picker UI — e.g. "GitHub",
"Cloudflare DNS". Should be short enough to fit in a list item without truncation
(under 40 characters recommended). |
| widgets[].description | required | string | One-sentence description of what this widget does, displayed beneath the name in the picker. Maximum 120 characters recommended. |
| widgets[].icon | required | string | Path to this widget's icon — typically "/widget/<id>/icon.svg" for
multi-widget path-namespaced containers. Must be an absolute path served by this same
container. The host may display this icon in the picker alongside the name. |
| widgets[].manifest | required | string | Absolute path to this widget's WCP manifest endpoint — e.g.
"/widget/github/wcp". After the user selects a widget from the directory,
the host calls this path to load the full manifest and complete the add-widget flow. |
| widgets[].tags | optional | string[] | Optional categorisation tags — e.g. ["devops", "monitoring"]. Hosts may
use tags to filter a large directory. No controlled vocabulary is defined; container authors
choose their own. |
Example 1 — Dual-widget container
A container serving both a GitHub widget and a Cloudflare widget from a single process on
port 3744. Each widget has its own path namespace under /widget/. The directory
lists both, and each entry's manifest field points directly to the correct
manifest path.
GET http://192.168.1.42:3744/wcp
HTTP/1.1 200 OK
Content-Type: application/json
{
"type": "directory",
"wcp": "1.4.0",
"widgets": [
{
"id": "github",
"uuid": "5da9e94a-bd56-45c5-9b46-5271d6b07f1b", ← server UUID (not a component UUID)
"name": "GitHub",
"description": "Browse all GitHub repositories for the authenticated account, sorted by last push.",
"icon": "/widget/github/icon.svg",
"manifest": "/widget/github/wcp"
},
{
"id": "cloudflare",
"uuid": "48834df0-69b3-478a-bac4-7b07a6d7a02e", ← server UUID (not a component UUID)
"name": "Cloudflare",
"description": "Cloudflare Workers, Domains, and DNS records in a dedicated orchestration.",
"icon": "/widget/cloudflare/icon.svg",
"manifest": "/widget/cloudflare/wcp"
}
]
}
When the host receives this response it shows a picker with two entries. The user selects GitHub. The host then calls:
GET http://192.168.1.42:3744/widget/github/wcp
↳ Returns the full WCP manifest for the GitHub widget
From this point the add-widget flow is identical to the existing single-widget flow.
The host stores widgetId = "github" alongside the stave's
wcpInstanceId and begins sending Wcp-Widget-Id: github on all
subsequent requests to this container.
Example 2 — Large multi-widget container with tags
A container that aggregates five DevOps monitoring widgets — each independently configurable and independently placeable on staves. Tags are used so that a host can filter the picker by category.
GET http://192.168.1.42:3750/wcp
{
"type": "directory",
"wcp": "1.4.0",
"widgets": [
{
"id": "cpu-monitor",
"uuid": "8d1519f6-8414-4954-8ab1-d5d283b0e3a5",
"name": "CPU Monitor",
"description": "Real-time CPU usage across all cores with per-core breakdown and load average.",
"icon": "/widget/cpu-monitor/icon.svg",
"manifest": "/widget/cpu-monitor/wcp",
"tags": ["monitoring", "system"]
},
{
"id": "memory-monitor",
"uuid": "401f8f4c-5fe6-4615-b830-523e7f9cf803",
"name": "Memory Monitor",
"description": "Physical and virtual memory usage with swap breakdown and per-process top consumers.",
"icon": "/widget/memory-monitor/icon.svg",
"manifest": "/widget/memory-monitor/wcp",
"tags": ["monitoring", "system"]
},
{
"id": "disk-io",
"uuid": "e79f0aeb-ed94-4b92-9ece-194243f0a4fd",
"name": "Disk I/O",
"description": "Read/write throughput and IOPS per mounted volume, updated every five seconds.",
"icon": "/widget/disk-io/icon.svg",
"manifest": "/widget/disk-io/wcp",
"tags": ["monitoring", "storage"]
},
{
"id": "network-traffic",
"uuid": "9e54833c-df85-4c0a-8a94-a5b4433e3dcc",
"name": "Network Traffic",
"description": "Inbound and outbound bandwidth per interface with running totals and packet error rates.",
"icon": "/widget/network-traffic/icon.svg",
"manifest": "/widget/network-traffic/wcp",
"tags": ["monitoring", "network"]
},
{
"id": "log-tail",
"uuid": "45037265-ea6d-4e8a-b2bf-eaf2551c7f61",
"name": "Log Tail",
"description": "Streaming tail of any configured log file or journald unit, with level-based colour coding.",
"icon": "/widget/log-tail/icon.svg",
"manifest": "/widget/log-tail/wcp",
"tags": ["monitoring", "logs"]
}
]
}
Example 3 — Single-widget container advertising itself via directory
A new single-widget container that chooses to implement the directory endpoint anyway. This is the recommended approach for all new containers, even single-widget ones — it allows the host to display a consistent picker UI and provides explicit documentation of what the container serves, rather than relying on the fallback behaviour.
GET http://192.168.1.42:3739/wcp
{
"type": "directory",
"wcp": "1.4.0",
"widgets": [
{
"id": "weather-ticker",
"uuid": "baf86656-53d3-4a7c-8998-5c2de2b99951",
"name": "Weather Ticker",
"description": "Live weather conditions for any location via the Open-Meteo API. Supports °C and °F.",
"icon": "/widget/icon.svg",
"manifest": "/widget/wcp"
}
]
}
↳ manifest path "/widget/wcp" — the standard single-widget location.
No path namespacing needed; this container has only one widget.
The host receives a directory with one entry. It may skip the picker and proceed directly to
the manifest fetch, or it may show the picker with one entry — both are valid host behaviours.
Because there is only one widget, no Wcp-Widget-Id header disambiguation is
required, though compliant hosts should still send it.
Example 4 — Legacy container: 404 fallback
An existing WCP 1.3.1 container — deployed before the directory specification was introduced
— does not implement GET /wcp. When the host queries the directory endpoint, it
receives a 404 Not Found. The host falls back immediately to
GET /widget/wcp and proceeds as before. No changes are required on the server;
no changes are visible to the user.
Step 1 — Host attempts directory lookup
GET http://192.168.1.42:3740/wcp
HTTP/1.1 404 Not Found
↳ No directory endpoint — this is a legacy WCP 1.3.1 container
Step 2 — Host falls back to direct manifest fetch
GET http://192.168.1.42:3740/widget/wcp
HTTP/1.1 200 OK
Content-Type: application/json
↳ Returns WCP 1.3.1 manifest — host proceeds with single-widget add flow
Host discovery algorithm
The following pseudocode describes the complete host-side discovery flow, covering all cases from a fully compliant WCP 1.4.0 container down to a pre-WCP container with no manifest endpoint at all.
async function discoverWidget(baseUrl):
// Step 1: try the WCP 1.4.0 container directory
response = await GET(baseUrl + "/wcp")
if response.ok:
directory = await response.json()
if directory.type === "directory" and directory.widgets.length > 1:
// Multi-widget container — show picker, wait for user selection
selected = await showWidgetPicker(directory.widgets)
manifestPath = selected.manifest
widgetId = selected.id
else if directory.type === "directory" and directory.widgets.length === 1:
// Single-entry directory — proceed without picker
manifestPath = directory.widgets[0].manifest
widgetId = directory.widgets[0].id
else:
// Unexpected response — treat as fallback
goto fallback
else if response.status === 404:
// Legacy container — no directory endpoint
goto fallback
else:
// Error (5xx, network failure, etc.) — surface to user
throw "Could not reach container directory"
goto fetchManifest(manifestPath, widgetId)
fallback:
// Step 2 (fallback): try the standard single-widget manifest
manifestPath = "/widget/wcp"
widgetId = null
fetchManifest(manifestPath, widgetId):
// Step 3: fetch the manifest and proceed with the standard add-widget flow
manifest = await GET(baseUrl + manifestPath).json()
return { manifest, widgetId }
Path namespacing in multi-widget containers
When a single process serves more than one widget, the widget endpoint sets must be
distinguishable by URL path so that a client calling /widget/health receives
a response that is unambiguously associated with a specific widget. WCP 1.4.0 defines the
standard path namespace convention for multi-widget containers:
Single-widget container — standard paths (unchanged from WCP 1.3.1):
/widget/ ← compact iframe
/widget/wcp ← manifest
/widget/health ← health check
/widget/icon.svg ← icon
/widget/configure ← configuration (POST)
Multi-widget container — namespaced paths per widget:
/widget/github/ ← GitHub compact iframe
/widget/github/wcp ← GitHub manifest
/widget/github/health ← GitHub health check
/widget/github/icon.svg ← GitHub icon
/widget/github/configure ← GitHub configuration (POST)
/widget/cloudflare/ ← Cloudflare compact iframe
/widget/cloudflare/wcp ← Cloudflare manifest
/widget/cloudflare/health ← Cloudflare health check
/widget/cloudflare/icon.svg ← Cloudflare icon
/widget/cloudflare/configure ← Cloudflare configuration (POST)
The id field in each directory entry is the path segment used in the namespace.
A directory entry declaring "id": "github" and "manifest": "/widget/github/wcp"
tells the host that the GitHub widget's full endpoint set lives under
/widget/github/. The host constructs all subsequent URLs by appending the
standard endpoint suffixes to the widget's base path — exactly as it would for a single-widget
container, using /widget/github/ instead of /widget/.
"manifest": "/widget/github/wcp", the host strips wcp from the
end to obtain the base path /widget/github/. It then forms all other endpoint
URLs as /widget/github/health, /widget/github/configure, and so on.
This rule means the container author does not need to declare every individual endpoint path
in the directory — only the manifest path.
The Wcp-Widget-Id header
Once a widget has been selected from the directory, the host sends
Wcp-Widget-Id on every subsequent request to that container — including the
manifest fetch, configuration POST, the iframe load, and all server-side proxy calls the
host makes on the widget's behalf. The value is the id field from the selected
directory entry.
After the user selects "github" from the picker:
GET /widget/github/wcp
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
Wcp-Widget-Id: github
Wcp-Version: 1.4.0
POST /widget/github/configure
Content-Type: application/json
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
Wcp-Widget-Id: github
Wcp-Version: 1.4.0
GET /widget/github/
Wcp-Instance-Id: b4f3a1c2-9e87-4d56-a012-3f8e7c9b1d04
Wcp-Widget-Id: github
Wcp-Version: 1.4.0
For path-namespaced containers, Wcp-Widget-Id is technically redundant — the
path already identifies the widget. Nevertheless, containers SHOULD validate
the header when it is present and MAY use it as a secondary routing or logging
signal. Sending it is mandatory for compliant hosts regardless of whether the container uses
path namespacing or header-based routing, because it makes widget identity explicit and
auditable at the HTTP layer.
GET /wcp returned 404), no
Wcp-Widget-Id header is sent. The absence of the header is the signal to the
container that it is being addressed as a WCP 1.3.1 single-widget container. A WCP 1.4.0
container that receives no Wcp-Widget-Id header on a request to
/widget/wcp must handle it as a single-widget request for backwards compatibility.
Header-only routing (alternative to path namespacing)
A container author who cannot or does not wish to use path namespacing may instead route
all requests through the standard /widget/ paths and use Wcp-Widget-Id
to determine which widget to serve. In this model, GET /widget/wcp returns a
different manifest depending on the value of Wcp-Widget-Id:
Header-only routing — both widgets share the /widget/ namespace:
GET /widget/wcp
Wcp-Widget-Id: github
↳ returns the GitHub manifest
GET /widget/wcp
Wcp-Widget-Id: cloudflare
↳ returns the Cloudflare manifest
In this case the directory entries declare the same manifest path for all
widgets, and the container routes on the header:
{
"type": "directory",
"wcp": "1.4.0",
"widgets": [
{
"id": "github",
"uuid": "5da9e94a-bd56-45c5-9b46-5271d6b07f1b",
"name": "GitHub",
"description": "GitHub repository browser.",
"icon": "/widget/icon/github.svg",
"manifest": "/widget/wcp" ← same path for both widgets
},
{
"id": "cloudflare",
"uuid": "48834df0-69b3-478a-bac4-7b07a6d7a02e",
"name": "Cloudflare",
"description": "Cloudflare Workers, Domains, and DNS.",
"icon": "/widget/icon/cloudflare.svg",
"manifest": "/widget/wcp" ← same path for both widgets
}
]
}
Header-only routing is valid but discouraged. Path namespacing is the recommended approach
because it makes the container's structure self-describing, allows independent health checks
per widget, avoids routing logic inside the container, and works correctly with any HTTP client
regardless of whether it supports the Wcp-Widget-Id header.
CORS update for WCP 1.4.0
The GET /wcp directory endpoint must include the same
Access-Control-Allow-Origin: * header as all other WCP endpoints. The
Wcp-Widget-Id header must also be added to Access-Control-Allow-Headers
on all widget endpoints. The complete required CORS declaration for a WCP 1.4.0 container is:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Wcp-Instance-Id, Wcp-Dashboard-Id, Wcp-Version, Wcp-Widget-Id
Backwards compatibility summary
| Container type | GET /wcp | Host behaviour | Wcp-Widget-Id sent |
|---|---|---|---|
| WCP 1.4.0 · multi-widget | 200 directory, multiple entries | Show picker → user selects → fetch manifest at declared path | Yes — value = selected widget id |
| WCP 1.4.0 · single-widget (explicit) | 200 directory, one entry | Skip picker (or show one-entry picker) → fetch manifest | Yes — value = the one widget's id |
| WCP 1.3.1 · legacy single-widget | 404 | Fall back to GET /widget/wcp — existing flow unchanged |
No |
| Pre-WCP · no manifest endpoint | 404 | Fall back to GET /widget/wcp → also 404 → surface error to user |
No |
Versioning
Protocol Versions
| Version | Status | Key additions |
|---|---|---|
| 1.0.0 | Superseded | Mandatory endpoints, manifest, postMessage types |
| 1.1.0 | Superseded | Widget configuration schema (autocomplete, select, number, text); POST /widget/configure; GET /widget/api/search proxy |
| 1.4.0 | Current | Server-level uuid field (required, distinct from component UUIDs); Container Directory (GET /wcp); multi-widget containers; path namespacing convention (/widget/<id>/); Wcp-Widget-Id request header; host discovery algorithm; GET /widget/api/guids extended to include server UUID; CORS update |
| 1.3.1 | Superseded | Orchestration schema: staves/instruments; location-search → autocomplete; compound autocomplete field param; full endpoint response contracts; /widget/manifest deprecated; WCP request headers (Wcp-Instance-Id, Wcp-Dashboard-Id, Wcp-Version); multi-instance server model; Design Guide terminology & stave metaphor |
| 1.3.0 | Superseded · Breaking | Components array mandatory; flat "widget" object removed; all role declarations move into components entries |
| 1.2.0 | Superseded | views object, mobile-portrait & mobile-landscape endpoints, Application Transformer Icon pattern |
Docker Image Tags
Use a compound tag encoding widget version and WCP protocol version:
# Format: {widget-version}-wcp{wcp-version}
docker tag myimage penrithbeacon/wcp-widget-example:1.0.0-wcp1.1.0
docker tag myimage penrithbeacon/wcp-widget-example:latest
Docker Convention
Image Naming
penrithbeacon/wcp-widget-{name}:{widget-ver}-wcp{wcp-ver}
penrithbeacon/wcp-widget-{name}:latest
Ports
WCP widgets do not have prescribed default ports — port assignment is a deployment decision made by the administrator. The WCP Bonjour service (port 3737) is the only reserved port; it advertises the host and port of every registered widget, so clients never need to know port numbers in advance.
| Port | Widget | Notes |
|---|---|---|
| 3737 | reserved WCP Bonjour | The only reserved port in WCP — service discovery authority |
| 3738–3740 | Penrith Beacon reference widgets | Ports used in the reference implementation — not a standard; bind your containers to any available port |
Reference Docker Hub images:
- penrithbeacon/wcp-widget-qr-generator
- penrithbeacon/wcp-widget-weather-ticker
- penrithbeacon/wcp-widget-theme-studio
Minimal docker-compose.yml
services:
my-widget:
image: penrithbeacon/wcp-widget-example:latest
container_name: wcp-widget-example
ports:
- "374X:374X"
restart: unless-stopped
volumes:
- widget_data:/app/data
volumes:
widget_data:
Widget Design Guide
This guide is not part of the WCP protocol itself — it is a practical reference for developers and designers building widgets that will be hosted in WCP-compatible dashboards. Following these conventions ensures your widget looks intentional, feels native, and works correctly across all supported sizes.
Language & Abstraction in WCP
WCP deliberately uses terminology that does not prescribe a particular user interface. The words orchestration, stave, and instrument were chosen because they carry clear meaning without implying any specific layout mechanism — giving architects, designers, and developers the freedom to present widgets in whatever way best suits their application.
The reasoning behind each term:
- Instrument — In software engineering, instrumentation has a well-established meaning: the practice of measuring and monitoring system behaviour through dedicated readouts — graphs, gauges, status indicators, live feeds, event counters. An IT instrument is anything that surfaces data or state for the operator to act on. In WCP, every widget is an instrument in exactly this sense: a self-contained panel that presents information or interaction to the user. The word does not imply any particular size, shape, or layout technology.
- Stave — In a musical score, a stave is the set of horizontal lines on
which the notation for a specific instrument — or instrument section — is written.
This is the critical insight: in a full orchestral score, you do not just see notes on
lines. You see a stave for the violins, a stave for the cellos, a stave for the brass,
and so on. Each stave belongs to its instruments. The horizontal lines of the
stave define the space in which those instruments are expressed — their rows. The beat
divisions running vertically across the score define the positional columns. In this
light, an orchestral score is already a grid: rows give you the vertical dimension of
the instrument space, columns give you the horizontal positions. The notes you see are
not separate from the instruments — they are the instruments, placed at
specific positions within that grid.
WCP borrows this directly. A WCP stave is the abstract layout area where IT instruments are placed. The rows of the grid correspond to the horizontal lines of the musical stave; the columns are the positional divisions that allow instruments to be positioned and sized across the available space. Just as a musical stave does not prescribe which instruments play — only where they are placed — a WCP stave does not prescribe which widgets are shown, only that there is a structured area in which they live. One concrete realisation of a stave is a column-and-row grid, but any layout system that positions instruments within a defined area satisfies the concept. The stave keeps the protocol neutral: a host can implement staves as grid pages, canvas regions, scrollable panels, or any other arrangement without violating the protocol. - Orchestration — An orchestration is the top-level configuration snapshot that brings everything together: its staves, instruments, masthead, and display metadata. The word is deliberate: just as an orchestral conductor brings together disparate instruments into a coherent whole for a specific purpose, a WCP orchestration brings together disparate widgets into a coherent dashboard for a specific purpose. Within an orchestration, collections of instruments can be further grouped into sub-orchestrations — nested logical groupings that the host may present as folders, tabs, pages, or any other navigation structure. The protocol does not specify the UI; it specifies the structure.
Grid-based layout: the standard stave implementation
When a stave is implemented as a grid, the mapping from the musical metaphor to the
layout system is direct: the rows of the grid correspond to the horizontal
lines of the musical stave — the space each instrument occupies vertically. The
columns are the positional divisions across the width of the stave — the
equivalent of beat positions in the score, dividing the horizontal space into addressable
units. An instrument declared as 4 × 2 occupies four column units wide and
two row units tall: four beats across, two lines deep.
WCP defines a standard grid to ensure that instrument developers can declare meaningful default sizes that work predictably across different host implementations. The standard grid is:
- 12 columns — fluid-width; each column is approximately 8.3% of the available stave width
- 100px row height — fixed; each row unit is 100px tall
This standard is a reference point, not a constraint. A host designed for a 49-inch curved
monitor may extend the grid to 24 or more columns; a compact panel host may use 4 columns and
60px rows. WCP instrument sizes — expressed as {w, h} grid units — scale
proportionally to whatever grid the host provides.
Tip: You don't need a running dashboard to use the Theme Studio. Once the container is running, navigate directly to
http://<host>:<port>/widget/full in any browser to open the full
studio as a standalone application — where <host> is the hostname or
IP address of the device on your local network running the container, and
<port> is whatever port your administrator has bound the container to.
For example: http://192.168.1.42:3740/widget/full or
http://nas.local:3740/widget/full — substitute the actual host and port
for your deployment. Port numbers are a deployment decision; the WCP Bonjour service
is responsible for advertising where each widget can be found.
Bookmark it during early design — you can create, preview, and export themes
before your dashboard or widget exists.
Sizing & The Grid
WCP instruments declare their size in grid units — columns wide and rows tall. The standard WCP grid uses a 12-column fluid layout with a 100px fixed row height. Columns are percentage-based; their pixel width scales with the available stave area.
defaultSize expressed as {w, h}
grid units scales proportionally to whatever grid the host provides.
Standard Grid Dimensions
| Axis | Standard unit | How it scales |
|---|---|---|
| Width | 1 of 12 columns = 8.3% | Fluid — scales with stave width |
| Height | 1 row = 100px | Fixed — 100px per row unit at standard scale |
Common Sizes & Use Cases
The following sizes are expressed in the standard 12-column grid. They are starting points — a host with a wider or narrower grid will scale them accordingly.
| Columns × Rows | % width (std) | Height (std) | Best for |
|---|---|---|---|
| 1 × 1 | 8% | 100px | Single metric, icon indicator, status dot |
| 4 × 2 | 33% | 200px | Compact status instrument, small chart, quick-action panel |
| 4 × 4 | 33% | 400px | Control panel, settings instrument, vertical list |
| 6 × 2 | 50% | 200px | Half-width instrument, medium chart |
| 8 × 2 | 67% | 200px | Wide chart, data table, timeline |
| 8 × 4 | 67% | 400px | Rich data visualisation, map, complex UI |
| 12 × 2 | 100% | 200px | Full-width banner, scrolling ticker strip |
| 12 × 4 | 100% | 400px | Full-width application panel |
| 12 × 12 | 100% | 1200px | Full embedded application — equivalent to a complete app UI at standard scale |
Setting Your Default Size
Declare your instrument's preferred grid size in the WCP manifest under defaultSize
in the component entry. Choose the minimum size at which your instrument is still fully functional
and readable:
// Inside a "components" entry
{
"id": "main",
"uuid": "f9c0f6bd-2e4e-448a-97e1-8bc55296aa55",
"role": "widget",
"defaultSize": { "w": 4, "h": 4 }
}
Users can always resize your instrument after adding it. The default is just the starting point.
Aspect Ratios
- Square (1:1 columns:rows at equal scale) — suits clocks, gauges, donut charts
- Wide and shallow (e.g. 12×1, 8×2) — suits tickers, progress bars, status strips
- Tall and narrow (e.g. 4×4, 4×6) — suits vertical lists, control panels, menus
- Wide and tall (e.g. 8×4, 12×6) — suits data tables, charts, full app panels
Masthead Ticker WCP 1.3.0
A masthead ticker fills the host's masthead strip — a thin horizontal band that runs the
full width of the dashboard, always visible above all content. Ticker components are declared
with "role":"ticker" and "mastheadCapable":true in the manifest.
Dimensions
| Property | Value |
|---|---|
| Width | 100% of available masthead space (after controls are placed) |
| Height | 40–60px (host-controlled; declare your preferred range in size) |
Design rules
- Display only — no interactive controls, buttons, or inputs; the ticker is a read strip
- No scrollbars — content must self-animate using CSS marquee or JS scroll
- Single line — text must not wrap; use
white-space: nowrapandoverflow: hidden - Font size: 12–14px — legible at masthead scale
- Theme-aware — use
var(--text),var(--muted),var(--bg); or transparent background - Lightweight — the masthead is always rendered; avoid heavy computation, large images, or network polling more frequent than every 30 seconds
Manifest declaration
// In "components" array (WCP 1.3.0)
{
"id": "my-ticker",
"uuid": "727d7610-78f7-4445-8cfe-a9781be86c80",
"name": "Weather Ticker",
"role": "ticker",
"path": "/widget/ticker",
"mastheadCapable": true,
"masthead": {
"height": { "min": 40, "max": 60 },
"width": { "min": 160, "max": 240 }
}
}
Masthead Control WCP 1.3.0
A masthead control occupies a slot in the masthead — to the left or right of the ticker strip. Unlike a ticker, a control can be interactive. Typical uses: status LEDs, play/stop buttons, volume sliders, radio station selectors, IoT monitors, single-icon launchers, or composite controls combining several of these into one panel.
Size
A masthead control has a fixed height equal to the masthead height
(40–60px, declared in size.min and size.max). Its
width is determined by the designer — from a minimum square
(width = height, the smallest practical size for a single-element indicator) up to whatever
the design requires.
- Narrow (square) — a single LED, a single icon button, a state indicator. Width = height. This is the minimum.
- Medium — a play/stop button with a volume control beside it. Width is two to three times the height.
- Wide — a composite radio station control containing a station selector button, start/stop, and a volume slider — all within a single control component designed to sit in the masthead. Width may be many times the height.
- Panel-based — a Control Panel placed in the masthead, containing multiple individual controls assembled side by side. The panel itself is the masthead control; its width is the sum of its contents.
The host subtracts the total width of all controls on each side from the available masthead width; the ticker occupies the remaining space in between.
Design rules
- Height is fixed by the host — design to fill the full masthead height; never overflow it
- At minimum width (square), icon-only — at 40–60px square, only an icon or colour-state is legible; never rely on text labels at minimum width
- Colour communicates state — use green/red/amber to indicate status (on/off, ok/warning/alarm) — the LED pattern works at any width
- Composite controls are single components — a radio control with play/stop/volume/selector is one WCP component; the internal layout is the designer's responsibility
- Position is host-configured — the manifest does not specify left or right; that is the user's choice in dashboard settings
- Resource-efficient — masthead controls are always visible; avoid heavy repaints, large assets, or frequent polling
mastheadCapable flag
Only controls with "mastheadCapable": true appear in the host's Masthead Controls
settings list. Controls without this flag appear in the Controls (C) orchestration only and
are available for Control Panels — not the masthead.
Manifest declaration
{
"id": "radio-ctrl",
"uuid": "355d7738-abc3-4ef7-a641-3c45594c4f51",
"name": "Radio Control",
"role": "control",
"path": "/widget/control/radio",
"mastheadCapable": true,
"masthead": {
"height": { "min": 40, "max": 60 },
"width": { "min": 160, "max": 240 }
}
}
Control Panel WCP 1.3.0
A Control Panel is a stave instrument type (not a widget endpoint) that hosts a configurable grid of individual control components. It is to controls what the stave is to instruments — a free-placement surface for assembling multiple controls into a coherent UI.
How it works
- User adds a Control Panel to the stave (configurable columns × rows, like any instrument)
- The panel starts empty
- User adds controls to the panel — either from the Controls (C) folder by drag-drop, or via Add → Control
- Each control occupies a cell within the panel's internal grid
- Controls can be cherry-picked from any control widget — including individual components from a multi-component server
Example use case
An IoT monitoring server exposes three control components: a temperature LED, a pump toggle, and a humidity display. The user creates a 3×1 Control Panel and places all three side by side. A plain text label component (a generic built-in label, manually named) sits below each LED to identify what it represents. The result is a compact IoT status strip on the stave.
Application Transformer Icons WCP 1.2.0
Every WCP component — regardless of role — is representable as a 1×1 icon card.
The host dashboard generates this card automatically from the component's name and
icon declared in the manifest. No additional endpoint is required.
The /widget/icon.svg mandatory endpoint (SVG, infinitely scalable; PNG minimum 256×256)
is sufficient. The dashboard renders the icon at the appropriate size and displays the name below it.
A 1×1 grid cell (8% wide × 100px tall) is the defined size for an Application Transformer Icon — a clickable icon that represents a widget in a launcher or discovery context.
Application icons behave differently from regular widgets:
- Click — opens the widget's full-page view (
wcp:open-window) immediately. No interaction happens within the 1×1 cell itself. - Deploy to a user orchestration — the icon transforms: the widget manifests at its declared
defaultSizeas a full instrument in the target orchestration. If the server exposes multiple components, a picker appears letting the user choose which component to instantiate.
Recommended 1×1 Icon HTML
/widget/, /widget/full, /widget/mobile-portrait, etc.)
are HTTP responses served by your container. They must be complete HTML documents so they:
render correctly when opened directly in a browser; include the viewport meta tag (essential for mobile views);
and declare a charset to prevent encoding issues.
The dashboard's own HTML Snippet card type uses bare fragments intentionally — the host injects them via
srcdoc and provides the document shell. That is the
only context where a fragment is appropriate.
Keep the compact view minimal — just an icon, a tooltip title, and a click handler, wrapped in a full HTML document:
<!-- /widget/ — compact view, served as a full HTML page -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Widget</title>
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; }
.icon-wrap {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
cursor: pointer; gap: 4px;
background: var(--surface, #161b22);
transition: background .15s;
}
.icon-wrap:hover { background: var(--surface2, #1c2128); }
.icon-wrap img { width: 40px; height: 40px; border-radius: 10px; }
.icon-wrap span { font-size: 10px; color: var(--muted, #8b949e);
white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; max-width: 90%; }
</style>
</head>
<body>
<div class="icon-wrap" title="Open My Widget"
onclick="window.parent.postMessage({type:'wcp:open-window',page:'full'},'*')">
<img src="/widget/icon.svg" alt="">
<span>My Widget</span>
</div>
</body>
</html>
Applications & Bonjour System Orchestrations
WCP-compatible hosts typically implement two auto-populated system orchestrations using this pattern:
- Bonjour — populated exclusively from the WCP Bonjour discovery service; all authorised widgets appear as 1×1 icons
- Applications — contains everything in Bonjour, plus any instrument added anywhere else in the host; a complete inventory
Users cannot manually add instruments to system orchestrations; they are maintained
automatically by the host. Deploying a 1×1 icon from a system orchestration into a user
orchestration transforms it to its full defaultSize.
Responsive Behaviour
Your widget's compact view (/widget/) runs inside a sandboxed iframe.
The iframe is resized by the user — your widget must fill its allocated space exactly
and respond correctly to any valid grid size.
Essential CSS
/* Always set these on your widget's root */
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden; /* prevent scrollbars inside compact view */
box-sizing: border-box;
}
.widget-root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
width: 100%, height: 100%, and relative units throughout.
Minimum Practical Sizes
Design with a minimum in mind — the smallest size at which your widget is still useful:
- Anything below 2 columns × 1 row (≈200px × 100px) is too small for general interactive content — with one important exception: the Application Transformer Icon pattern, which intentionally uses a 1×1 grid cell
- 4 × 2 (≈33% × 200px) is the practical minimum for most useful widgets
- Set
widget.defaultSizeto your minimum functional size, not your ideal size
Scrollbars in Compact View
Avoid scrollbars in the compact widget view — they signal that content overflows the allocated space,
which feels broken. Design the compact view to fit within its grid cell.
If you have more content, expose it through the full-page view (/widget/full)
opened via wcp:open-window or wcp:open-tab.
Full-Page View (/widget/full)
The full-page view opens in a dedicated native window at the dimensions you specify in the manifest.
This is where you can show the complete application experience without space constraints.
Use height: 100% cascaded from html, body — never 100vh
inside an Electron-hosted window (see Optional Endpoints).
Mobile Portrait View WCP 1.2.0
Served at /widget/mobile-portrait, this view targets phones and narrow
devices held vertically. WCP-compatible hosts detect the device and orientation and
load this view automatically when appropriate.
Mobile Portrait Grid
| Property | Value | Notes |
|---|---|---|
| Columns | 4 | ~90px per column on a 360px phone |
| Row height | 60px | More compact than desktop 100px |
| Typical width | 360–430px | Most Android and iPhone widths in portrait |
| Max practical cols | 4 | More than 4 columns is unreadable at mobile scale |
Design Constraints
- No horizontal scroll — content must fit within 4 columns; never assume more width
- Touch-first — tap targets minimum 44px; avoid hover states as the primary interaction
- Single-column layouts preferred — stack content vertically rather than side by side
- Large readable text — minimum 14px; 16px for primary content
- Avoid dense data tables — use summary cards with tap-to-expand instead
Detection & Fallback
Hosts detect device type via User-Agent and screen dimensions. If a widget does not declare
views.mobile-portrait in its manifest, the host falls back to the compact iframe view
(/widget/) scaled to fit — which typically looks poor on mobile. Implementing mobile views
is strongly recommended for any publicly accessible widget.
<!-- /widget/mobile-portrait — full HTML page -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- viewport meta is mandatory for mobile views -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Widget</title>
<style>
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
html, body {
width:100%; height:100%; overflow:hidden;
font-size: 16px; /* larger base for mobile readability */
-webkit-text-size-adjust: 100%;
background: var(--bg, #ffffff);
color: var(--text, #1f2328);
}
button, a { min-height: 44px; } /* touch target minimum */
</style>
</head>
<body>
<!-- your portrait layout here -->
</body>
</html>
Mobile Landscape View WCP 1.2.0
Served at /widget/mobile-landscape, this view targets phones and small tablets
held horizontally. More horizontal space is available, but vertical space is limited.
Mobile Landscape Grid
| Property | Value | Notes |
|---|---|---|
| Columns | 8 | More space than portrait, less than desktop |
| Row height | 60px | Same as portrait — consistent vertical rhythm |
| Typical width | 640–900px | Most phones in landscape |
| Typical height | 360–430px | Very limited — design for shallow layouts |
Design Constraints
- Shallow layouts — landscape has very limited height; avoid tall stacked content
- Two-column layouts work well — side-by-side panels suit the wide/short aspect
- No vertical scroll if possible — landscape sessions are brief; all critical content should be above the fold
- Reuse portrait components — landscape is often a side-by-side arrangement of portrait elements
- Touch targets — same 44px minimum as portrait
Tablet Considerations
The desktop view (served via /widget/full) is appropriate for tablets
in both orientations when a full dashboard is being used. The mobile landscape view is specifically
for phones in landscape and small 7–8" tablets where the 12-column desktop grid would be too dense.
Theme Compatibility
WCP dashboards use a CSS custom property theming system. If your widget reads these variables, it will automatically match whatever theme the user has selected — including all 15 themes in the WCP Theme Studio and any custom theme they create.
CSS Custom Properties
| Variable | Purpose | Dark default |
|---|---|---|
| --bg | Page/window background | #0d1117 |
| --surface | Card / panel background | #161b22 |
| --surface2 | Input / nested surface | #1c2128 |
| --border | Borders and dividers | #30363d |
| --text | Primary text | #e6edf3 |
| --muted | Secondary / de-emphasised text | #8b949e |
| --accent | Brand colour, buttons, highlights | #f0883e |
| --green | Success, running, positive | #3fb950 |
| --red | Error, stopped, negative | #f85149 |
| --yellow | Warning, pending | #d29922 |
| --blue | Info, links | #58a6ff |
| --radius | Border radius for cards/buttons | 8px |
| --shadow | Box shadow for elevated surfaces | 0 4px 16px rgba(0,0,0,.45) |
Using Theme Variables in Your Widget
/* Your widget will automatically match the host theme */
body {
background: var(--bg, #0d1117); /* fallback for standalone viewing */
color: var(--text, #e6edf3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.card {
background: var(--surface, #161b22);
border: 1px solid var(--border, #30363d);
border-radius: var(--radius, 8px);
}
.btn-primary {
background: var(--accent, #f0883e);
color: #0d1117;
}
Penrith Beacon WCP Native Themes
Three themes match the Penrith Beacon WCP Dashboard exactly. Encourage users who want consistent look-and-feel across multiple dashboards and widgets to select one of these as their base:
| Theme | ID | Best for |
|---|---|---|
| Penrith Beacon WCP Dark | pb-wcp-dark | Default dark environment |
| Penrith Beacon WCP Light | pb-wcp-light | Light/daytime use |
| Penrith Beacon WCP High Contrast | pb-wcp-hc | Accessibility, high ambient light |
All three are available in the
WCP Theme Studio
(v1.1.0+) and shareable as .pbtheme.json URLs.
Dashboard UX Conventions
- Base font size: 13px — keep text legible at this scale; avoid fonts smaller than 11px
- No modal dialogs in compact view — modals in iframes are clipped by the grid cell; use the full-page view instead
- No native alerts or confirms —
window.alert()is blocked in sandboxed iframes; use inline status messages - Prefer dark backgrounds — the majority of dashboard users use dark themes; test dark first, light second
- Touch-friendly tap targets — minimum 32px for interactive elements, as dashboards may run on tablets
WCP File Formats
WCP defines four portable file formats for distributing containers, orchestrations, and themes. All four are zip archives with a specific internal structure and file extension that hosts and tools can detect and act on.
| Extension | Contents | Purpose |
|---|---|---|
.wcp | Container manifest + icon + docs | Export a single WCP container for sharing or archiving |
.wcpo | One or more orchestrations | Export complete dashboard configurations: staves, instruments, orchestrations, and masthead |
.wcpt | Theme definitions with UUIDs | Export custom themes for sharing across dashboards or organisations |
.wcpx | Bundle of .wcpo and/or .wcpt files | Single distribution envelope for org-wide rollout |
.wcpx format can bundle both together for recipients who need everything in one file.
.wcp — Container Package
A .wcp file is a zip archive that captures a WCP container at a point in time.
It is generated by the container itself via the GET /widget/export.wcp endpoint.
Zip structure
manifest.json ← full WCP manifest (with server UUID and component UUIDs)
icon.svg ← widget icon
DOCKER.md ← human-readable documentation
README.md ← optional additional notes
manifest.json (package level)
{
"wcp": "1.3.0", // WCP protocol version
"name": "Radio",
"created": "2026-06-01T10:00:00Z",
"author": "Anthony Harrison", // optional
"authorEmail": "anthony@example.com" // optional
"authorUrl": "https://example.com" // optional — website for this container
}
New endpoint: GET /widget/export.wcp
Every WCP 1.3.0+ container should implement this endpoint. It returns the .wcp
zip with Content-Disposition: attachment; filename="[widget-name].wcp".
New endpoint: GET /widget/api/guids
Returns the server UUID and all component UUIDs this container exposes. Used by WCP Bonjour for GUID-based discovery — the server UUID identifies the container as a whole, and the component UUIDs identify each individual component within it:
{
"uuid": "f786d19d-8e9f-4d8d-86fa-4c4c956b027e", ← server UUID
"components": [
{ "id": "radio-player", "uuid": "fb11989e-c443-4171-9387-068025ded7a4", "name": "Radio Player" },
{ "id": "radio-led", "uuid": "67c3fb15-eb48-4f60-a7fc-32b9e0a20032", "name": "Playing LED" }
]
}
.wcpo — Orchestration Package
An Orchestration is a named, saveable snapshot of a complete dashboard configuration: all staves (each containing their instruments and orchestrations), masthead configuration (tickers and controls), and display metadata. Themes are referenced by UUID — not embedded.
Zip structure
manifest.json ← package metadata
orchestrations/
[id].json ← one file per orchestration (schema below)
icons/
[id]-512.png ← custom orchestration icon (512×512)
[id]-256.png ← custom orchestration icon (256×256)
README.md
manifest.json (package level)
{
"wcpo": "1.1",
"created": "2026-06-01T10:00:00Z",
"count": 2,
"files": ["orchestrations/pb-default.json", "orchestrations/pb-work.json"],
"author": "Anthony Harrison", // optional — person who assembled this package
"authorEmail": "anthony@example.com", // optional
"authorUrl": "https://example.com" // optional
}
Orchestration JSON schema
{
"id": "pb-default",
"name": "Default Orchestration",
"displayName": "My Dashboard", // shown in masthead logo
"icon": null, // null = default app icon
"created": "2026-06-01T10:00:00Z",
"appVersion": "1.3.7",
"staves": [
{
"id": "pb_main",
"name": "Stave 1",
"folders": [...], // orchestrations / sub-orchestrations
"instruments":[...], // instruments with widgetUrl if URLs included
// snippet instruments: html → htmlBase64 (base64 UTF-8)
"rootOrder": [...]
}
],
"masthead": {
"tickers": [...], // html tickers: html → htmlBase64 (base64 UTF-8)
"controls": [...],
"mode": "multiple",
"cycleDuration": 30
},
"themeRefs": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
// UUIDs of referenced themes
"author": "Anthony Harrison", // optional — person who assembled this package
"authorEmail": "anthony@example.com", // optional
"authorUrl": "https://example.com" // optional
}
widgetUrl fields are included
in exported stave instruments. This is correct for intranet sharing and backups where all
recipients share the same network. When distributing across organisations with different
URL schemes, exclude URLs at export time — the recipient's Bonjour service can resolve the
correct local URLs from the component UUIDs.
.wcpo v1.1, all inline HTML content
is base64-encoded (UTF-8) for safe JSON transport. This affects:
- HTML Snippet instruments — field
htmlBase64(replaceshtml) - Masthead tickers of type
"html"— same field rename
htmlBase64 with standard base64/UTF-8 decoding.
For backward compatibility, importers should also accept the legacy html field
(present in v1.0 exports).
.wcpt — Themes Package
A .wcpt file exports themes with their permanent UUIDs. All themes the
dashboard knows about are exportable — regardless of where they came from (a theme
studio widget, a URL import, or manual creation). The export wizard presents the full list
with all themes selected by default; the user deselects those they don't need.
The .wcpt file itself is assigned a collection UUID at export
time, making the collection independently referenceable. The author can give it a meaningful
name (e.g. "Acme Corp Branding Q2 2026") in the export wizard.
Zip structure
manifest.json ← collection metadata (UUID, name, author)
themes.json ← array of theme objects with UUIDs
manifest.json schema
{
"wcpt": "1.0",
"collectionUuid": "550e8400-e29b-41d4-a716-446655440001", // generated at export
"collectionName": "Acme Corp Branding",
"created": "2026-06-01T10:00:00Z",
"author": "Anthony Harrison", // optional — person who assembled this collection
"authorEmail": "anthony@example.com", // optional
"authorUrl": "https://acme.com/themes" // optional — corporate page, intranet, etc.
}
themes.json schema
[
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Dark",
"author": "Design Team", // optional — creator of this specific theme
"authorEmail": "design@acme.com", // optional
"authorUrl": "https://acme.com/brand", // optional — theme page, corporate brand guide, etc.
"vars": { "--bg": "#0d1117", "...": "..." }
}
]
author, authorEmail, and authorUrl fields when
a user creates or edits a custom theme. These fields can be left empty and filled in later.
On export, the wizard shows each theme's attribution so the user can review or complete it
before distributing. The authorUrl is particularly useful for themes that
represent corporate branding — it can point to an intranet brand guide, a public website,
or a dedicated theme showcase page.
When a .wcpo orchestration references a theme UUID via themeRefs,
the receiving dashboard checks whether that UUID is installed. Missing themes are listed in the
post-import configuration checklist with a suggestion to obtain the appropriate .wcpt file.
.wcpx — Distribution Envelope
A .wcpx file is a distribution envelope that bundles multiple WCP format files
together. Use it when you need to distribute both orchestrations and themes in a single package
— for example, to standardise look-and-feel alongside workflow configuration across an organisation.
Zip structure
manifest.json ← envelope manifest (lists contents)
orchestrations/
[name].wcpo ← one or more orchestration packages
themes/
[name].wcpt ← one or more theme packages
README.md
manifest.json (envelope level)
{
"wcpx": "1.0",
"created": "2026-06-01T10:00:00Z",
"contents": [
{ "type": "wcpo", "file": "orchestrations/company-workflows.wcpo" },
{ "type": "wcpt", "file": "themes/company-branding.wcpt" }
],
"author": "Anthony Harrison", // optional
"authorEmail": "anthony@example.com" // optional
"authorUrl": "https://example.com" // optional
}
When importing a .wcpx file, the host inspects the envelope manifest and presents
the user with a unified import UI showing all included packages. The user selects which
orchestrations and which theme sets to import.
.wcpx format is versioned — the
contents array is designed to accommodate additional format types as the WCP
specification expands beyond its initial .wcpo and .wcpt support.
Future releases may introduce new containable format types within the same envelope structure.
WCP Bonjour Coming Soon — Port 3737
WCP Bonjour is the zero-configuration service discovery component of WCP — inspired by Apple's Bonjour networking protocol. Running on port 3737, it maintains a registry of available WCP instruments, enabling host dashboards to discover and load authorised instruments automatically — without manual URL configuration.
The Bonjour service itself may be deployed in a number of ways: as a Docker or Kubernetes-managed container on the local network, as a dedicated local process, or as a remote service reachable over the internet. In all cases the host connects to it at the reserved address. Critically, the Bonjour service does more than list what is already running — it typically knows the source location of each instrument's container image, enabling it to provision and start a container on demand when authorised by an administrator. This means a dashboard importing an orchestration can request that the Bonjour service resolve and instantiate any instruments referenced in that orchestration, rather than requiring the operator to install containers manually.
Planned Capabilities
- Instrument registry — searchable catalogue of known WCP instruments with cached manifests
- Authorisation control — administrators select which instruments are available in this installation
- Auto-population — host dashboards fetch the Bonjour manifest to populate system orchestrations with all authorised instruments in one step, without manual configuration
- On-demand provisioning — when an instrument is not yet running, Bonjour can retrieve its image and instantiate a container, then return the live URL to the dashboard
- Health monitoring — tracks reachability of all registered instruments
- Admin UI — browser-based management interface
Planned Manifest Extension
// GET http://192.168.1.10:3737/widget/wcp (Bonjour service on the local network)
{
"wcp": "1.1.0",
"name": "WCP Bonjour",
"version": "1.0.0",
"role": "registry",
"widgets": [
{ "url":"http://192.168.1.42:3739", "authorised":true, "manifest":{ /* cached */ } }
]
}
Full specification will be published at widgetcontextprotocol.com/#bonjour when the container is released.
Changelog
WCP 1.3.1 — 2026-06
- Orchestration schema:
pinboards→staves. The top-level array in orchestration JSON is now namedstaves. This aligns the schema with WCP's abstract terminology — a stave is a layout area, not a UI metaphor. - Instrument array:
links→instruments. Within each stave, the widget/instrument array is now namedinstruments. The old keylinksis accepted on import for backward compatibility. - Default stave name: newly created staves default to "Stave", "Stave 2", etc. — replacing "Pinboard".
- Design Guide additions: Language & Abstraction section introduced — explains the orchestration/stave/instrument metaphor, IT instrumentation as a discipline, and the rationale for implementation-neutral terminology.
- Sizing & Grid: reframed as a standard reference grid (12-column, 100px rows) rather than a fixed requirement; hosts may use different column counts and row heights for their target display.
- WCP request headers defined:
Wcp-Instance-Id(required),Wcp-Dashboard-Id(optional),Wcp-Version(optional). All three follow RFC 6648 naming — noX-prefix. - Multi-instance server model: a single widget container can now serve
independent configurations for multiple dashboard placements simultaneously, keyed by
Wcp-Instance-Id. Instance IDs are stored per-instrument in stave data and stripped from.wcpoexports. - Configuration lifecycle clarified: configure POST stores config by instance ID; subsequent widget GET injects config into HTML as JavaScript variables; widget JS carries its own state for downstream API calls — server storage after serve is optional.
- Security layering: WCP headers coexist with standard
Authorization: Bearerheaders; security layer is orthogonal to the instance mechanism. autocompleteconfig type replaceslocation-search; compound autocomplete supported via&field=<id>parameter; full/widget/api/searchand/widget/configurecontracts defined.- Endpoint response contracts defined for all mandatory and optional endpoints;
/widget/manifestdeprecated.
WCP 1.3.0 — 2026-05
- Components array — mandatory, breaking change.
"components"is now a required field in every WCP 1.3.0 manifest. The flat top-level"widget"object is removed from the protocol. All role declarations (widget, control, ticker) move inside"components"entries using the"role"field. - Component roles — each component declares
"role": "widget" | "control" | "ticker" mastheadobject replacessize— masthead-capable components declaremasthead.height(range in px) and optionallymasthead.width(range in px; absent on tickers which fill available space elastically).defaultSizeremains the stave placement field.- Masthead Ticker —
"ticker"role withmastheadCapable: true; endpoint/widget/ticker; height 40–60px; display-only horizontal strip - Masthead Control —
"control"role with optionalmastheadCapable: true; endpoint/widget/control/<id>; always square at masthead height; icon-first design; position (left/right) is host-configured - Control Panel — host-side pinboard card type for assembling multiple controls into a configurable grid
- A / B / C system orchestrations — Applications, Bonjour, Controls; auto-populated; not manually editable; distinguished from user-managed orchestrations
- 1×1 icon generation — host generates 1×1 cards from manifest
name+icon; no additional endpoint required - Auto-distribution — on Bonjour import, each component distributed to its role-appropriate system orchestration automatically
WCP 1.2.0 — 2026-05
- New
viewsobject in WCP manifest — declares adaptive renderings for desktop, mobile-portrait, mobile-landscape - New optional endpoints:
/widget/mobile-portrait(4-col/60px grid) and/widget/mobile-landscape(8-col/60px grid) - Application Transformer Icon pattern — defined 1×1 grid cell as the standard widget launcher/icon format
- Hosts select view automatically based on detected device type and orientation
- Widgets without mobile views fall back to compact iframe view (scaled) — mobile views strongly recommended for public-facing widgets
WCP 1.1.0 — 2026-05
- Added
configfield to WCP manifest — declarative configuration schema - New field type:
autocomplete(originally introduced aslocation-search) with server-side proxy; renamed toautocompletein WCP 1.3.1 - New field types:
select,number,text - Host proxies config submissions via
POST /api/widget-configure - Host proxies location searches via
GET /api/widget-search - Compound version tag convention:
{widget-ver}-wcp{wcp-ver} - Port 3737 reserved for WCP Bonjour discovery service
- File formats:
.wcp(container),.wcpo(orchestration),.wcpt(themes),.wcpx(distribution envelope) — all zip archives with defined schemas - New endpoints:
GET /widget/export.wcp(container export),GET /widget/api/guids(GUID list for Bonjour discovery) - Component
uuidfield — permanent identifier for GUID-based discovery and orchestration theme references
WCP 1.0.0 — 2026-05
- Initial specification
- Mandatory endpoints:
/widget/,/widget/wcp,/widget/health,/widget/icon.svg - postMessage types: open-window, open-tab, copy-to-clipboard, download-file, import-theme
- WCP manifest schema with pages and actions
About
The Widget Context Protocol was designed and developed by Penrith Beacon® as an open standard for local-first dashboard widget communication.
WCP is free to implement. Reference widgets and the reference host dashboard are published on Docker Hub and GitHub under the penrithbeacon namespace.
Penrith Beacon® is a registered trademark. The WCP specification itself carries no licence restrictions.