WCP 1.5.0

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.

Current version: WCP 1.5.0. This document is the authoritative specification. WCP is free to implement — no licence required.

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.

New to building WCP widgets? The WCP Developer's Guide is the practical companion to this specification — worked examples, implementation patterns, and step-by-step walkthroughs for building your first widget.

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

This section describes a reference implementation — not a protocol requirement. WCP does not prescribe how hosts organise or present their component inventory. Another implementation might express these same distinctions through search filters, tags, separate screens, or any other navigation structure. The value of describing this implementation is to illustrate the practical consequences of the protocol's role differentiation — to use it as a working example, not a mandate.

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.

The picker is the manifest. The multi-component picker is the direct expression of the "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/:

EndpointMethodDescription
/widget/GETCompact widget HTML — loaded by the dashboard in an iframe
/widget/wcpGETWCP manifest (JSON) — the widget's self-description
/widget/healthGETHealth check — returns {"status":"ok","name":"<name>"}
/widget/icon.svgGETWidget icon (SVG preferred, PNG fallback)
CORS required: All endpoints must respond with Access-Control-Allow-Origin: * and handle OPTIONS preflight requests with 204 No Content.

Response Contracts

EndpointHTTP statusContent-TypeResponse body
GET /widget/200text/html Complete HTML document — must include <!DOCTYPE html>, <meta charset>, and <meta name="viewport">. Not a fragment.
GET /widget/wcp200application/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.svg200image/svg+xml or image/png SVG: vector, any declared dimensions — scales cleanly to any host size. PNG fallback: minimum 256×256 px.

Optional Endpoints

EndpointMethodDescription
Manifest
/widget/manifestGETDeprecated — WCP 1.3.0. Pre-WCP host compatibility shim. New implementations must not implement this. Hosts must not call it.
Configuration
/widget/configurePOSTAccept 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/searchGETServer-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/fullGETFull-page desktop view — complete HTML document, opened in a new window or tab by the host
/widget/mobile-portraitGETMobile portrait view — complete HTML document, 4-column / 60px-row grid WCP 1.2.0
/widget/mobile-landscapeGETMobile landscape view — complete HTML document, 8-column / 60px-row grid WCP 1.2.0
Masthead
/widget/tickerGETMasthead ticker view — complete HTML document, thin horizontal strip, display-only WCP 1.3.0
/widget/control/<id>GETIndividual 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.wcpGETExport this container as a .wcp package (zip: manifest + icon + docs) WCP 1.3.0
/widget/api/guidsGETReturn the server UUID and all component UUIDs — used by Bonjour for GUID-based discovery WCP 1.3.0
API
/widget/api/*anyWidget-specific API routes
Chromium-embedded host note: Many desktop dashboard applications — including well-known tools like Visual Studio Code, Slack, Discord, and Figma — are built using Electron, a framework that embeds the Chromium browser engine in a native desktop shell. WCP hosts built on Electron open /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

FieldRequiredDescription
wcprequiredProtocol version — currently "1.4.0"
uuidrequiredPermanent 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
namerequiredHuman-readable widget name
versionrequiredWidget version (semver)
descriptionrequiredOne or two sentences
iconrequiredPath to icon (relative to widget origin)
healthrequiredPath to health endpoint
componentsrequiredArray of component descriptors — declares all widgets, controls, and tickers this server exposes. See Components Array.
pagesoptionalFull-page views the host can open in a new window or tab
actionsoptionalAction buttons shown on the dashboard card
configoptionalConfiguration schema — fields the host renders as a settings form (WCP 1.1.0)
views.desktopoptionalPath to full desktop view (WCP 1.2.0)
views.mobile-portraitoptionalPath to mobile portrait view — 4-col/60px (WCP 1.2.0)
views.mobile-landscapeoptionalPath 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.
Team consistency. Because "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.
Breaking change — no backwards compatibility. The "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.

LevelFields
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

FieldRequiredDescription
idrequiredUnique identifier within this server's component list
namerequiredHuman-readable display name — used for 1×1 icon label and picker
rolerequired"widget" | "control" | "ticker"
pathrequiredURL path to this component's HTML view
iconoptionalIcon path for this component; falls back to server-level icon
renderModeoptional"iframe" (default) — applies to "widget" role
defaultSizeoptionalGrid size {w,h} — applies to "widget" role
mastheadCapableoptionaltrue if this component can appear in the masthead — applies to "control" and "ticker" roles
uuidrequiredPermanent 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.
mastheadoptionalMasthead rendering dimensions — see below. Required when mastheadCapable: true.
masthead.heightoptional{"min":40,"max":60} — height range in px. Host renders at masthead bar height, clamped to this range.
masthead.widthoptional{"min":160,"max":240} — width range in px for "control" role. Absent on tickers — they fill remaining space elastically.
Server UUID and component UUIDs must always be distinct — no exceptions. A server UUID identifies the container as a whole. A component UUID identifies one specific component within that container. These are different entities at different levels of the hierarchy and must never share a UUID value. A server that starts life with a single component might gain additional components in a future version — if the server UUID and the original component UUID were the same, that UUID would become ambiguous: does it refer to the server, or to the original component? Assigning distinct UUIDs from the start eliminates this problem permanently. Generate two separate UUID v4 values — one for the server, one for each component — and commit both to the codebase.

Auto-distribution on import

When a WCP Bonjour service imports a server, each component is distributed to the appropriate system orchestration automatically:

Component roleSystem orchestrationAlso in
"widget"Bonjour (B)Applications (A)
"control"Controls (C)Applications (A)
"control" with mastheadCapable: true + masthead objectControls (C) + Masthead Controls settingsApplications (A)
"ticker" with mastheadCapable: true + masthead objectMasthead Tickers settingsApplications (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, '*').

typeRequired fieldsHost action
wcp:open-windowpage, width?, height?Opens named page in a new native window
wcp:open-tabpage, tab:{title,icon}Opens page as a persistent dashboard tab
wcp:copy-to-clipboardtextCopies text — bypasses iframe clipboard sandbox
wcp:download-filefilename, content, mimeTypeTriggers file save dialog
wcp:import-themeurl (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!' }, '*');
Security: Hosts validate that events originate from a known widget origin. 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 the options array in the config entry; no query to the widget is needed.
  • number — a numeric input. The min and max values 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

typeDescriptionRequired extra fields
autocompleteServer-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
selectDropdown with a fixed list of options declared in the manifest.options: [{value, label}]
numberNumeric input with optional bounds.min, max, step
textFree-text input.placeholder, maxLength
passwordFree-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.

Why this matters. An exported orchestration is intended to be shareable — sent to a colleague, published to a registry, or imported on another machine. Credentials must never leave the dashboard that created them. The 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
[]
ParameterRequiredDescription
qrequiredThe user's current typed input. May be empty string on initial focus.
fieldoptionalThe 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.

HeaderRequiredDescription
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.

Instance IDs are dashboard-local and never exported. When an orchestration is exported as a .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:

  1. Dashboard POSTs configuration with Wcp-Instance-Id: <uuid> → widget server stores config keyed by that UUID
  2. Dashboard GETs /widget/ with the same Wcp-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>
  3. Widget JavaScript uses WCP_CONFIG directly 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-Aapp-1 orch-A:app-1
Second application window, same orchestration orch-Aapp-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

FieldRequiredTypeDescription
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/.

The widget base path is derived from the manifest path. Given "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.

Wcp-Widget-Id is NOT sent for legacy containers. When a host falls back to the WCP 1.3.1 single-manifest flow (because 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 typeGET /wcpHost behaviourWcp-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

VersionStatusKey additions
1.0.0SupersededMandatory endpoints, manifest, postMessage types
1.1.0SupersededWidget configuration schema (autocomplete, select, number, text); POST /widget/configure; GET /widget/api/search proxy
1.4.0CurrentServer-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.1SupersededOrchestration schema: staves/instruments; location-searchautocomplete; 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.0Superseded · BreakingComponents array mandatory; flat "widget" object removed; all role declarations move into components entries
1.2.0Supersededviews 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.

PortWidgetNotes
3737reserved WCP BonjourThe only reserved port in WCP — service discovery authority
3738–3740Penrith Beacon reference widgetsPorts used in the reference implementation — not a standard; bind your containers to any available port

Reference Docker Hub images:

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.
Why abstract language matters. Terminology like "tab", "folder", "card", "pinboard", or "page" encodes a specific UI solution into the protocol itself — which then constrains every host implementation to follow that solution. By using terms like stave and instrument, WCP leaves the presentation layer entirely to the host. A stave is whatever area a host uses to place instruments. How that area is navigated, labelled, or rendered is the host's design decision. The protocol's job is to define the structure, not the surface.

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.

Try themes before you build. The WCP Theme Studio widget lets you explore all built-in themes and create your own. Pull it from Docker Hub and add it to any WCP dashboard to design with the real colour tokens before writing a line of widget code.

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.

The standard grid is a reference, not a constraint. Hosts are free to implement staves with different column counts or row heights to suit their target display — for example, a 24-column grid for a 49-inch curved monitor, or a 4-column / 60px-row grid for a compact panel. An instrument's defaultSize expressed as {w, h} grid units scales proportionally to whatever grid the host provides.

Standard Grid Dimensions

AxisStandard unitHow it scales
Width1 of 12 columns = 8.3%Fluid — scales with stave width
Height1 row = 100pxFixed — 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 × 18%100pxSingle metric, icon indicator, status dot
4 × 233%200pxCompact status instrument, small chart, quick-action panel
4 × 433%400pxControl panel, settings instrument, vertical list
6 × 250%200pxHalf-width instrument, medium chart
8 × 267%200pxWide chart, data table, timeline
8 × 467%400pxRich data visualisation, map, complex UI
12 × 2100%200pxFull-width banner, scrolling ticker strip
12 × 4100%400pxFull-width application panel
12 × 12100%1200pxFull embedded application — equivalent to a complete app UI at standard scale
12 × 12 = full application at standard scale. There is no upper limit on rows or columns. A fully embedded application spanning the entire stave is intentional — WCP is designed to host both compact instruments and complete applications on the same stave. On a host with 96 columns and 24 rows (designed for a large curved display), a 12 × 12 instrument would occupy one quarter of the stave — the absolute pixel area scales with the host's grid, not the column count.

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

PropertyValue
Width100% of available masthead space (after controls are placed)
Height40–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: nowrap and overflow: 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

  1. User adds a Control Panel to the stave (configurable columns × rows, like any instrument)
  2. The panel starts empty
  3. User adds controls to the panel — either from the Controls (C) folder by drag-drop, or via Add → Control
  4. Each control occupies a cell within the panel's internal grid
  5. 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.

Labels are generic. A label control is a plain text display with no programmatic link to other controls — the user types the label text and positions it manually. The connection between a label and an LED is purely visual/spatial.

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 defaultSize as a full instrument in the target orchestration. If the server exposes multiple components, a picker appears letting the user choose which component to instantiate.
Application icons are not interactive in-place. They are transformers — either launching a full window or morphing into a full widget on drop. This is why 1×1 is viable despite its tiny size: no readable text or controls need to fit inside it. Think of the macOS Dock or an iOS home screen grid.

Recommended 1×1 Icon HTML

Always use a full HTML page for widget endpoints — not a bare HTML fragment. Widget endpoints (/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;
}
Never hardcode pixel dimensions inside a compact widget. A widget hardcoded to 400px wide will break at 3-column or 12-column sizes. Use 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.defaultSize to 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

PropertyValueNotes
Columns4~90px per column on a 360px phone
Row height60pxMore compact than desktop 100px
Typical width360–430pxMost Android and iPhone widths in portrait
Max practical cols4More 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

PropertyValueNotes
Columns8More space than portrait, less than desktop
Row height60pxSame as portrait — consistent vertical rhythm
Typical width640–900pxMost phones in landscape
Typical height360–430pxVery 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

VariablePurposeDark default
--bgPage/window background#0d1117
--surfaceCard / panel background#161b22
--surface2Input / nested surface#1c2128
--borderBorders and dividers#30363d
--textPrimary text#e6edf3
--mutedSecondary / de-emphasised text#8b949e
--accentBrand colour, buttons, highlights#f0883e
--greenSuccess, running, positive#3fb950
--redError, stopped, negative#f85149
--yellowWarning, pending#d29922
--blueInfo, links#58a6ff
--radiusBorder radius for cards/buttons8px
--shadowBox shadow for elevated surfaces0 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;
}
Always provide fallback values. When your widget is opened standalone (e.g. directly in a browser for testing), the host's CSS custom properties won't be injected. Fallbacks ensure the widget looks correct in both contexts.

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:

ThemeIDBest for
Penrith Beacon WCP Darkpb-wcp-darkDefault dark environment
Penrith Beacon WCP Lightpb-wcp-lightLight/daytime use
Penrith Beacon WCP High Contrastpb-wcp-hcAccessibility, 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 confirmswindow.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.

ExtensionContentsPurpose
.wcpContainer manifest + icon + docsExport a single WCP container for sharing or archiving
.wcpoOne or more orchestrationsExport complete dashboard configurations: staves, instruments, orchestrations, and masthead
.wcptTheme definitions with UUIDsExport custom themes for sharing across dashboards or organisations
.wcpxBundle of .wcpo and/or .wcpt filesSingle distribution envelope for org-wide rollout
All formats use UUID references. Orchestrations reference themes by UUID rather than embedding theme data. This keeps orchestration files small and allows themes to be updated independently. The .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
}
Widget URLs in exports. By default, 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.
HTML encoding (v1.1+). From .wcpo v1.1, all inline HTML content is base64-encoded (UTF-8) for safe JSON transport. This affects:
  • HTML Snippet instruments — field htmlBase64 (replaces html)
  • Masthead tickers of type "html" — same field rename
Importers should decode 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", "...": "..." }
  }
]
Theme editor responsibility. WCP-compatible theme editors should provide 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.

Future: Theme Services. The collection UUID enables a future pattern — theme service containers that serve collections remotely, analogous to WCP Bonjour for widget discovery. A dashboard could query a theme service by collection UUID, retrieve the full theme set, and install it automatically. Theme services could be daisy-chained using the same hop-limit and circular-reference-prevention model as Bonjour. This is a forward-looking design intent, not yet a specified endpoint.

.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.

Designed to evolve. The .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.

Port 3737 is reserved across all WCP installations. Do not assign other instruments to this port.

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: pinboardsstaves. The top-level array in orchestration JSON is now named staves. This aligns the schema with WCP's abstract terminology — a stave is a layout area, not a UI metaphor.
  • Instrument array: linksinstruments. Within each stave, the widget/instrument array is now named instruments. The old key links is 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 — no X- 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 .wcpo exports.
  • 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: Bearer headers; security layer is orthogonal to the instance mechanism.
  • autocomplete config type replaces location-search; compound autocomplete supported via &field=<id> parameter; full /widget/api/search and /widget/configure contracts defined.
  • Endpoint response contracts defined for all mandatory and optional endpoints; /widget/manifest deprecated.

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"
  • masthead object replaces size — masthead-capable components declare masthead.height (range in px) and optionally masthead.width (range in px; absent on tickers which fill available space elastically). defaultSize remains the stave placement field.
  • Masthead Ticker"ticker" role with mastheadCapable: true; endpoint /widget/ticker; height 40–60px; display-only horizontal strip
  • Masthead Control"control" role with optional mastheadCapable: 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 views object 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 config field to WCP manifest — declarative configuration schema
  • New field type: autocomplete (originally introduced as location-search) with server-side proxy; renamed to autocomplete in 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 uuid field — 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.