Skip to main content
Glama
yomgui

World Clocks

by yomgui
README.md
# World Clock Dashboard — example MCP App for OpenWork

A world clock dashboard with configurable cities, ticking local times, and a 12/24-hour display. The shared demo lets everyone edit one clock list. Per-member storage and token verification are implemented, but the authorization-server integration needed for settings to follow each member is not yet included.

Clocks are shown in time order; there is no manual ordering. On every client tick, clocks are sorted by their current UTC offset (earliest first, respecting DST), then by city name for ties. Your own zone has a **you** badge but is not pinned. If the visible limit is smaller than the saved set, the first N clocks in time order are shown; Edit shows the full set in the same order.

Preferences store a set of city ids plus `hourCycle` and `limit`, not a custom order. Existing stored preferences still load: legacy `order`/`position` fields and the old array ordering have no effect on display. Your cities and format are kept, but their visible order (and which fit within the limit) may change. New `save_preferences` calls reject `order` and `position`.

## How it is built

A TypeScript MCP server exposes tools and one `ui://` resource; React and Vite build a self-contained MCP App view. Browser `Intl` computes time and DST locally: the wire carries locations, not clock readings, and the view uses no browser storage. `api/mcp.js` loads the emitted server build for Vercel's shared-mode deployment.

From the repository root, using Node 24.20.0 and pnpm 11.4.0:

```sh
pnpm install --frozen-lockfile
pnpm run check                 # typecheck, build view, 118 tests
pnpm run build:server          # emitted Node build for api/mcp.js
AUTH_REQUIRED=false PORT=4321 pnpm run serve
```

The local endpoint is `http://localhost:4321/mcp`.

## Persistence

Vercel Blob when `BLOB_READ_WRITE_TOKEN` is set; file when `WORLD_CLOCKS_DATA_DIR` is set; memory otherwise. Boot logs and preference/render tool results expose `storeKind` (`blob`, `file`, or `memory`) without credentials. Connect a **private** Blob store to the Vercel project to inject the token.

Blob uses one private JSON object at `prefs/<key>.json`, deterministic names, overwrite enabled, last-write-wins, and uncached origin reads (`useCache: false`). SDK 2.8 supports private access; no public-read fallback is used. Keys are `sha256(iss|sub)` in authenticated mode and `shared` when `AUTH_REQUIRED=false`. This changes the old `anonymous` shared key and org-suffixed authenticated keys; existing records are not migrated automatically. An issuer's same subject shares preferences across org claims. Shared mode intentionally shares edits among all callers.

Memory resets on process restart/cold start; file storage requires a persistent volume. Blob survives Vercel instances, but does not itself add an authorization-server integration.

## Add it to an OpenWork dashboard

1. Deploy the shared demo with the included `vercel.json`, or host the server at a public HTTPS origin with `AUTH_REQUIRED=false`.
2. Open **Settings › Library → custom MCP**. Enter `https://your-host.example/mcp` and select Authentication **None** for shared mode.
3. On the dashboard, choose **Add app**. In Den's picker, the World Clocks connection currently shows **FOUR entries**:
   - **World Clocks** — `show_world_clocks`, the dashboard tile.
   - **Resolve locations** — `resolve_locations`, an internal app-only helper.
   - **Get my preferences** — `get_preferences`, an internal app-only helper.
   - **Save my preferences** — `save_preferences`, an internal app-only write helper.

Add ONLY the first card, "World Clocks" (Run automatically). The other three are internal helpers the tile calls itself; do not add them as tiles — "Save my preferences" would write on every refresh.

Launch with default `{}` and choose your cities inside the tile; explicit `cities` launch arguments override saved preferences.

The three helpers retain their `ui://world-clocks/mcp-app.html` binding because the released host requires it for calls from the tile. **Product finding:** `visibility: ["app"]` with `resourceUri` currently leaks these helpers into Den's Add app picker. The picker should hide app-only helpers without removing the resource binding needed by the host; this is guidance, not a product fix.

For a future per-member deployment, supply an authorization server issuing JWTs specifically for this app. Set `AUTH_REQUIRED=true`, `AUTH_ISSUER`, `AUTH_JWKS_URL`, **`AUTH_AUDIENCE`** (this server's resource URL), `PUBLIC_BASE_URL`, and persistent `WORLD_CLOCKS_DATA_DIR`; connect with `authType: oauth` and `credentialMode: per_member`. Tokens for another resource, audience-less tokens, and opaque tokens are rejected; there is no userinfo fallback. The embedded authorization server and end-to-end personal sign-in flow are not implemented in this World Clocks repository. For an OpenWork-identity OIDC implementation, see [yomgui/acme-home-demo](https://github.com/yomgui/acme-home-demo) (`IDENTITY_MODE=openwork`, `server/oauth.ts`, and `server/upstream.ts`), including its configuration and security caveats. That reference does not enable personal sign-in for World Clocks or establish hosted verification.

## License

[MIT](LICENSE) — Copyright (c) 2026 OpenWork Labs.