@nyuchi/mzizi-mcp 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +105 -39
  2. package/dist/auth/front.d.ts +8 -0
  3. package/dist/auth/front.d.ts.map +1 -1
  4. package/dist/auth/front.js +10 -0
  5. package/dist/auth/front.js.map +1 -1
  6. package/dist/auth/mcp-api-handler.d.ts +1 -1
  7. package/dist/auth/mcp-api-handler.d.ts.map +1 -1
  8. package/dist/auth/mcp-api-handler.js +4 -3
  9. package/dist/auth/mcp-api-handler.js.map +1 -1
  10. package/dist/auth/provider-options.d.ts +16 -0
  11. package/dist/auth/provider-options.d.ts.map +1 -0
  12. package/dist/auth/provider-options.js +21 -0
  13. package/dist/auth/provider-options.js.map +1 -0
  14. package/dist/env.d.ts +1 -0
  15. package/dist/env.d.ts.map +1 -1
  16. package/dist/floors.d.ts +117 -0
  17. package/dist/floors.d.ts.map +1 -0
  18. package/dist/floors.js +103 -0
  19. package/dist/floors.js.map +1 -0
  20. package/dist/generated/registry-pin.d.ts +8 -1
  21. package/dist/generated/registry-pin.d.ts.map +1 -1
  22. package/dist/generated/registry-pin.js +3 -1
  23. package/dist/generated/registry-pin.js.map +1 -1
  24. package/dist/generated/registry.d.ts +2 -2
  25. package/dist/generated/registry.js +198 -198
  26. package/dist/generated/registry.js.map +1 -1
  27. package/dist/generated/skills.d.ts +1 -1
  28. package/dist/generated/skills.js +13 -13
  29. package/dist/generated/skills.js.map +1 -1
  30. package/dist/http.d.ts +14 -4
  31. package/dist/http.d.ts.map +1 -1
  32. package/dist/http.js +38 -54
  33. package/dist/http.js.map +1 -1
  34. package/dist/origin.d.ts +55 -0
  35. package/dist/origin.d.ts.map +1 -0
  36. package/dist/origin.js +127 -0
  37. package/dist/origin.js.map +1 -0
  38. package/dist/rate-limit.d.ts +1 -1
  39. package/dist/rate-limit.d.ts.map +1 -1
  40. package/dist/rate-limit.js +3 -2
  41. package/dist/rate-limit.js.map +1 -1
  42. package/dist/server.d.ts +47 -3
  43. package/dist/server.d.ts.map +1 -1
  44. package/dist/server.js +14 -8
  45. package/dist/server.js.map +1 -1
  46. package/dist/tools.d.ts.map +1 -1
  47. package/dist/tools.js +115 -48
  48. package/dist/tools.js.map +1 -1
  49. package/dist/worker.d.ts.map +1 -1
  50. package/dist/worker.js +19 -11
  51. package/dist/worker.js.map +1 -1
  52. package/package.json +2 -2
package/README.md CHANGED
@@ -12,7 +12,7 @@ Owned and operated by **Mzizi**.
12
12
  ---
13
13
 
14
14
  **Live at** `https://mcp.mzizi.dev/mcp` (Cloudflare Worker `mzizi-mcp`, version
15
- 0.14.0 — `MZIZI_MCP_VERSION` in `src/server.ts` is what `serverInfo.version`
15
+ 0.15.0 — `MZIZI_MCP_VERSION` in `src/server.ts` is what `serverInfo.version`
16
16
  reports, and it moves in lockstep with `package.json` and `server.json`).
17
17
 
18
18
  ## Free, with no gate except Fundi
@@ -72,10 +72,13 @@ registration and does not publish a client ID metadata document.
72
72
 
73
73
  ### Per client
74
74
 
75
- Each client writes the entry above in its own format. The full guide, checked against each
76
- vendor's documentation, is
77
- [docs.mzizi.dev/toolchain/connect](https://docs.mzizi.dev/toolchain/connect). The short
78
- version:
75
+ Each client writes the entry above in its own format. The full guide, with the vendor page
76
+ each line was read from, is
77
+ [docs.mzizi.dev/toolchain/connect](https://docs.mzizi.dev/toolchain/connect); the table in this
78
+ repository's [README](../README.md#connect-a-client) has the config path and key for Claude
79
+ Code, Claude.ai and Desktop, Codex, Cursor, VS Code, Antigravity, Gemini CLI, Devin Desktop
80
+ (formerly Windsurf), Zed, Cline, OpenCode, Kiro and JetBrains. All of it is documented by the
81
+ vendors, read on 2026-11-03, and not run in the tools. The short version:
79
82
 
80
83
  ```bash
81
84
  # Claude Code: the server alone (skills over MCP) ...
@@ -84,11 +87,10 @@ claude mcp add --transport http mzizi https://mcp.mzizi.dev/mcp
84
87
  /plugin marketplace add mzizi-dev/mzizi-registry
85
88
  /plugin install mzizi@mzizi
86
89
 
87
- # Codex: the server alone, or the same plugin (mzizi-registry#520 adds its Codex manifest)
90
+ # Codex: the server (there is no Codex plugin until mzizi-registry#520 merges)
88
91
  codex mcp add mzizi --url https://mcp.mzizi.dev/mcp
89
- codex plugin marketplace add mzizi-dev/mzizi-registry
90
92
 
91
- # Gemini CLI
93
+ # Gemini CLI (enterprise; free and Pro/Ultra users moved to Antigravity CLI on 2026-06-18)
92
94
  gemini mcp add --transport http mzizi https://mcp.mzizi.dev/mcp
93
95
 
94
96
  # VS Code
@@ -104,10 +106,14 @@ code --add-mcp '{"name":"mzizi","type":"http","url":"https://mcp.mzizi.dev/mcp"}
104
106
  `mcp-client-2025-11-20` beta header.
105
107
  - **OpenAI Responses API and Agents SDK:** a tool
106
108
  `{"type": "mcp", "server_label": "mzizi", "server_url": "https://mcp.mzizi.dev/mcp"}`
107
- (`HostedMCPTool(tool_config=…)` in the SDK).
108
- - **Cursor** (`mcp.json`, `"url"`), **OpenCode** (`opencode.json`, `"type": "remote"`),
109
- **Windsurf** (`mcp_config.json`, `"serverUrl"`) and **Gemini CLI** (`settings.json`,
110
- `"httpUrl"`) each name the URL field differently.
109
+ (`HostedMCPTool(tool_config=...)` in the SDK).
110
+ - **The URL key differs by client:** `url` (Cursor, Zed, Kiro, JetBrains, Devin Desktop,
111
+ Cline, OpenCode), `serverUrl` (Antigravity), `httpUrl` (Gemini CLI `settings.json`). Cline
112
+ needs `"type": "streamableHttp"` set explicitly (without it, it falls back to SSE), and
113
+ Devin Desktop names the transport `transport`, not `type`.
114
+ - **Needs a bridge or stdio:** a client that only speaks stdio (older MCP clients, and Junie,
115
+ whose current help page does not confirm streamable HTTP) runs `npx -y @nyuchi/mzizi-mcp`
116
+ (stdio, below). `mcp-remote` is a third-party bridge this project has not tested.
111
117
 
112
118
  **Skills over MCP.** A client that cannot install the npm package (Claude.ai, ChatGPT, the
113
119
  APIs) gets the skills from `mzizi_get_skills`: no arguments lists them with their category,
@@ -120,8 +126,10 @@ SKILL.md body, `mzizi://skills/<name>/<path>` for each file, listed by `resource
120
126
  resources or offers prompts gets them without calling a tool. `catalogue.json` and
121
127
  `mzizi_mcp_describe` list every skill with its category and files. All of it is read from the
122
128
  skill files bundled at build time, and `test/skills-surfaces.test.ts` fails if any surface
123
- drifts from `mzizi-skills/`. Codex, Cursor, VS Code, Gemini CLI and OpenCode also read
124
- `.agents/skills/`, where `npx skills experimental_sync` links the npm bundle.
129
+ drifts from `mzizi-skills/`. Codex, Cursor, VS Code, Gemini CLI, Antigravity, Zed, OpenCode and
130
+ Devin Desktop also read `.agents/skills/`, where `npx skills experimental_sync` links the npm
131
+ bundle. For instruction formats that cannot hold a skill (Cursor rules, Antigravity and Devin
132
+ rules, `AGENTS.md`), see [`mzizi-skills/adapters/`](../mzizi-skills/adapters/README.md).
125
133
 
126
134
  How a skills change reaches this server: the skills are read from `../mzizi-skills/` in this
127
135
  repository at build time, not from npm and not from the registry pin. A change to a SKILL.md
@@ -129,6 +137,32 @@ merged to `main` redeploys `mcp.mzizi.dev` (Workers Builds), and that deploy ser
129
137
  publish of `@nyuchi/mzizi-skills` and the registry's dependency bump feed the plugin and
130
138
  `api.mzizi.dev/v1/skills`, not this server.
131
139
 
140
+ ### Compatibility with the MCP spec
141
+
142
+ Probed from outside with `curl` on 2026-11-03, with no `Origin` header and again with a CLI
143
+ user agent (`codex_cli_rs/0.1`), against the Streamable HTTP page of the 2026-07-28 revision
144
+ (<https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http>):
145
+
146
+ | Probe | Result |
147
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
148
+ | `initialize` with `2025-06-18`, `2025-11-25` | `200`, `application/json`, same version echoed, tools, resources and prompts with `listChanged`; no `Mcp-Session-Id` (stateless) |
149
+ | `initialize` with `2026-07-28` | `200`, answers `2025-11-25` (the legacy handshake is the highest it negotiates) |
150
+ | `server/discover` and `tools/list` with `_meta` and the headers | `200`; `supportedVersions: ["2026-07-28"]`, 14 tools |
151
+ | `tools/call` without `Mcp-Name` | `400`, JSON-RPC `-32020` `HeaderMismatch`, as the spec requires |
152
+ | `tools/list` with no `initialize` and no version header | `200` (treated as the earliest header-less version, which the spec allows) |
153
+ | notification POST (`notifications/initialized`) | `202`, no body |
154
+ | `GET` on the endpoint | `405` with `Allow: POST, OPTIONS` (the spec's answer for a server with no GET stream) |
155
+ | `Accept: application/json` alone | `406`; the client must list both `application/json` and `text/event-stream` |
156
+ | `MCP-Protocol-Version: 1999-01-01` | `400`, lists the supported versions (`2025-11-25` back to `2024-10-07`) |
157
+ | `Origin: http://evil.example`, `Origin: null` | `403` |
158
+ | `Origin: https://evil.example` | `200` on the deployed 0.14.0: any `https` origin is accepted. The spec says an invalid Origin MUST get `403`. The explicit allowlist merged to `staging` in #319 refuses it, and is live only after the next release. |
159
+ | `/mcp/signin` without a token | `401` with `WWW-Authenticate: Bearer ... resource_metadata=...`, the OAuth flow clients start from |
160
+
161
+ Deviations to know about: a JSON-RPC batch array (removed from the spec in 2025-06-18) answers
162
+ a single object, not an array, and `server/discover` lists only the modern version while the
163
+ legacy handshake still negotiates the older ones. No client was run; these are the server's
164
+ answers to a plain HTTP client.
165
+
132
166
  ### stdio (local / offline)
133
167
 
134
168
  Run the stdio bin via npx — useful for Claude Code, Cursor, or any assistant that
@@ -224,22 +258,22 @@ call, passing the result through untouched:
224
258
 
225
259
  ## Tools
226
260
 
227
- | Tool | Returns |
228
- | ---------------------------------- | ------------------------------------------------------------------------------ |
229
- | `mzizi_search` | Word search over components, conventions, AI instruction sets and releases |
230
- | `mzizi_get_component` | One component in full, **Rust first**; `include: ["docs", "versions"]` |
231
- | `mzizi_list_components` | The lean, paged index; filter by `node`, `owner`, `collection`, `type`, `rust` |
232
- | `mzizi_get_tokens` | Brand system; `family: "colors"` for all 21 colour families |
233
- | `mzizi_get_architecture` | The DNA helix, or one node/rung with `node` (uncapped) |
234
- | `mzizi_get_doctrine` | `ubuntu-pillars`, `ubuntu-principles`, `ai-instructions`, `conventions` |
235
- | `mzizi_get_skills` | The agent skills: listed, one by `name` (with `file`), or a `category` |
236
- | `mzizi_check_accessibility` | WCAG 2 contrast ratio and the Mzizi 7:1 floor, computed locally (not APCA) |
237
- | `mzizi_report_issue` | Your issue desk. **Sign-in** |
238
- | `mzizi_fundi` | Delegate to fundi (when the Worker can reach fundi). **Sign-in** |
239
- | `mzizi_mcp_describe` | This server's tools, their access, what each replaces, and data sources |
240
- | `docs_search_mzizi` | Search the documentation (forwarded) |
241
- | `docs_query_docs_filesystem_mzizi` | Read-only `rg`/`cat`/`tree` over the doc pages (forwarded) |
242
- | `docs_submit_feedback` | Report a documentation problem (forwarded) |
261
+ | Tool | Returns |
262
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
263
+ | `mzizi_search` | Word search over components, conventions, AI instruction sets and releases |
264
+ | `mzizi_get_component` | One component in full, **Rust first**; `include: ["docs", "versions"]` |
265
+ | `mzizi_list_components` | The lean, paged index; filter by `node`, `owner`, `collection`, `type`, `rust` |
266
+ | `mzizi_get_tokens` | Brand system; `family: "colors"` for all 21 colour families, `"floors"` for the Bundu floor |
267
+ | `mzizi_get_architecture` | The DNA helix, or one node/rung with `node` (uncapped) |
268
+ | `mzizi_get_doctrine` | `ubuntu-pillars`, `ubuntu-principles`, `ai-instructions`, `conventions`, `bundu-floor` |
269
+ | `mzizi_get_skills` | The agent skills: listed, one by `name` (with `file`), or a `category` |
270
+ | `mzizi_check_accessibility` | WCAG 2 contrast ratio and the Mzizi 7:1 floor, and spacing values against the spacing floor, computed locally (not APCA) |
271
+ | `mzizi_report_issue` | Your issue desk. **Sign-in** |
272
+ | `mzizi_fundi` | Delegate to fundi (when the Worker can reach fundi). **Sign-in** |
273
+ | `mzizi_mcp_describe` | This server's tools, their access, what each replaces, and data sources |
274
+ | `docs_search_mzizi` | Search the documentation (forwarded) |
275
+ | `docs_query_docs_filesystem_mzizi` | Read-only `rg`/`cat`/`tree` over the doc pages (forwarded) |
276
+ | `docs_submit_feedback` | Report a documentation problem (forwarded) |
243
277
 
244
278
  **How the tools behave (SDK v2, #295).**
245
279
 
@@ -257,13 +291,43 @@ call, passing the result through untouched:
257
291
  `mzizi_list_components` also keeps `hasMore` / `nextOffset` for older clients.
258
292
  - A domain failure (unknown component, bad colour, sign-in needed) is a tool result with
259
293
  `isError: true` and text that says how to recover, never a protocol error.
260
- - **Browser origins (Streamable HTTP §Security):** an `Origin` that does not parse (the opaque
261
- `null` included) or a plain-`http` origin other than localhost is refused with 403. Every
262
- `https` origin passes, because browser-based MCP clients on other sites use this public,
263
- cookie-less server. Clients that send no `Origin` (every non-browser client) are not affected.
294
+ - **Browser origins (Streamable HTTP §Security):** a request with an `Origin` header whose host is
295
+ not on the list is refused with 403 before any server is built, on `/mcp`, `/mcp/signin` and
296
+ every other path starting with `/mcp`, with or without a token, and before the anonymous rate
297
+ limit is charged. The code refuses a present but empty `Origin` (only a missing header counts as
298
+ no `Origin`), though workerd appears to deliver an empty header as absent. The list is `mzizi.dev`, `www.mzizi.dev`, `docs.mzizi.dev`, `app.mzizi.dev` and
299
+ `mcp.mzizi.dev`, each over `https` on the default port only (no `http`, no `:8443`, no other
300
+ subdomain), plus localhost on any port. Add more with the `MZIZI_ALLOWED_ORIGINS` variable,
301
+ comma-separated `https://host` origins (case and a trailing slash are tolerated; anything else
302
+ that is not a plain `https` origin is dropped with a logged warning). Clients that send no
303
+ `Origin` (every non-browser client) are not affected. On `/mcp`, CORS echoes an allowed `Origin`
304
+ with `Vary: Origin` and never answers `Access-Control-Allow-Origin: *`. The OAuth provider's own
305
+ endpoints (`/token`, `/register`, `/.well-known/*`) keep the provider's CORS behaviour, which
306
+ echoes the caller's `Origin`, and `/catalogue.json` is `*` on purpose. Browser clients hosted on
307
+ other sites cannot call `/mcp` (`src/origin.ts` is the one list).
308
+ - **PKCE and token audience:** the authorization-server metadata advertises `S256` only. Audience
309
+ binding applies only to clients that send a `resource` indicator (RFC 8707): such a token is
310
+ refused on a different server or path, but a token for `/mcp` is also accepted on
311
+ `/mcp/signin` (path-prefix matching, the same server). A token minted with no `resource` has no
312
+ audience and is served on `/mcp` and `/mcp/signin` (see the CHANGELOG, #295).
264
313
  - Calls to fundi and to the identity provider time out after 15 s.
265
314
  - `evals.xml` holds ten read-only questions with answers checked against production.
266
315
 
316
+ **The Bundu floor, and the spacing floor in it.** The registry's minimums nothing can go under are
317
+ served three ways, all read from the bundled registry's `/v1/brand` `accessibility` block at the
318
+ pinned commit (`minTouchTarget`, `minInset`, `minControlGap`, `minLabelGap`, `minItemGap`,
319
+ `minBodyLeading`), never typed in this repo (`src/floors.ts`): `mzizi_get_tokens family=floors` (also
320
+ `family=accessibility`, and `family=spacing`, which returns `spacingFloor` beside the scale),
321
+ `mzizi_get_doctrine topic=bundu-floor`, and the `floors` block of `mzizi_mcp_describe`, the
322
+ `mzizi://tools` resource and `GET /catalogue.json`. The spacing floor (owner, 2026-11-04,
323
+ mzizi-registry#617) is a 14px inset between text and its container edge (`--inset-floor`), 7px gaps between
324
+ adjacent controls, between a label and its field and between sibling items (`--gap-floor`), and 1.5
325
+ body leading (`--leading-floor`). `mzizi_check_accessibility` takes `insetPx`, `controlGapPx`,
326
+ `labelGapPx`, `itemGapPx` and `leading` beside (or instead of) the two colours, compares each with that
327
+ floor and returns `spacing: { floor, checks, meetsFloor }`. If the pinned registry commit predates the
328
+ floor, `floors.spacing` is `null`, the tool descriptions name no number, and a spacing check is a tool
329
+ error saying so (or, beside colours, a `spacingUnavailable` note); the tools never answer from a copy. To check a project's source rather than values, run `mzizi check` (`@nyuchi/mzizi-cli`).
330
+
267
331
  **Text colours from `mzizi_get_tokens`.** Every colour family carries `textLight` /
268
332
  `textDark`: use them for text in that colour on the base surface (they are
269
333
  `--color-<name>-text`). On a family's container, use `onContainerLight` /
@@ -319,6 +383,7 @@ tools present the Rust implementation first wherever one exists:
319
383
  project's own. Hash the registry source, not the installed file.
320
384
  - **Breaking for readers of 0.10.x:** the React body moved from `component` to
321
385
  `react`, and `renamedFrom` moved to the top level.
386
+
322
387
  - `mzizi_list_components` rows carry `rustCrate` when the component has Rust (absent
323
388
  otherwise), the response carries `withRust` (how many of `total` have Rust), and
324
389
  `rust: true` / `rust: false` filters on it.
@@ -370,11 +435,12 @@ at. See `AUTH.md`.
370
435
 
371
436
  Plain vars (`[vars]` in `wrangler.toml`, not secrets):
372
437
 
373
- | Var | Notes |
374
- | ---------------------- | ------------------------------------------------------------- |
375
- | `WORKOS_CLIENT_ID` | Connect OAuth app client id (public client; PKCE) |
376
- | `WORKOS_M2M_CLIENT_ID` | Fundi M2M app client id; used only on the HTTP + M2M fallback |
377
- | `FUNDI_URL` | Base URL of the fundi worker, for the HTTP + M2M fallback |
438
+ | Var | Notes |
439
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
440
+ | `WORKOS_CLIENT_ID` | Connect OAuth app client id (public client; PKCE) |
441
+ | `WORKOS_M2M_CLIENT_ID` | Fundi M2M app client id; used only on the HTTP + M2M fallback |
442
+ | `FUNDI_URL` | Base URL of the fundi worker, for the HTTP + M2M fallback |
443
+ | `MZIZI_ALLOWED_ORIGINS` | Optional, unset by default. Extra browser origins for `/mcp`, comma-separated `https://host` (e.g. `https://partner.example`); a public setting, so a var, not a secret. See "Browser origins" |
378
444
 
379
445
  Bindings (`wrangler.toml`): `FUNDI_SERVICE` (Service Binding to `mzizi-fundi`'s
380
446
  `FundiInternal` entrypoint; what makes fundi reachable in production),
@@ -4,6 +4,14 @@
4
4
  * Its own module, not `worker.ts`: the Worker's entry module may export only
5
5
  * handlers and Durable Object classes, and this is tested directly.
6
6
  */
7
+ /**
8
+ * True for the provider's API route as the PROVIDER matches it: any path that
9
+ * starts with `/mcp` (`/mcp`, `/mcp/signin`, `/mcpx`, `/mcp%2Fsignin`: the
10
+ * provider's `matchApiRoute` is a bare `startsWith`). The Origin gate in
11
+ * `worker.ts` uses this, so no path the provider treats as the API route
12
+ * escapes the gate.
13
+ */
14
+ export declare function isProviderApiPath(pathname: string): boolean;
7
15
  /**
8
16
  * True for a request the free tier serves: the MCP path, no credential at all.
9
17
  *
@@ -1 +1 @@
1
- {"version":3,"file":"front.d.ts","sourceRoot":"","sources":["../../src/auth/front.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAI/D"}
1
+ {"version":3,"file":"front.d.ts","sourceRoot":"","sources":["../../src/auth/front.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAI/D"}
@@ -6,6 +6,16 @@
6
6
  */
7
7
  /** The paths served to a caller with no token. `/mcp/signin` is deliberately not one. */
8
8
  const ANONYMOUS_PATHS = new Set(["/mcp", "/mcp/"]);
9
+ /**
10
+ * True for the provider's API route as the PROVIDER matches it: any path that
11
+ * starts with `/mcp` (`/mcp`, `/mcp/signin`, `/mcpx`, `/mcp%2Fsignin`: the
12
+ * provider's `matchApiRoute` is a bare `startsWith`). The Origin gate in
13
+ * `worker.ts` uses this, so no path the provider treats as the API route
14
+ * escapes the gate.
15
+ */
16
+ export function isProviderApiPath(pathname) {
17
+ return pathname.startsWith("/mcp");
18
+ }
9
19
  /**
10
20
  * True for a request the free tier serves: the MCP path, no credential at all.
11
21
  *
@@ -1 +1 @@
1
- {"version":3,"file":"front.js","sourceRoot":"","sources":["../../src/auth/front.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yFAAyF;AACzF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;QAAE,OAAO,KAAK,CAAC;IACvD,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC5D,CAAC"}
1
+ {"version":3,"file":"front.js","sourceRoot":"","sources":["../../src/auth/front.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yFAAyF;AACzF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;QAAE,OAAO,KAAK,CAAC;IACvD,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC5D,CAAC"}
@@ -24,7 +24,7 @@ type AuthedContext = ExecutionContext & {
24
24
  * Takes no `ctx` on purpose, so the anonymous front in `worker.ts` cannot pick
25
25
  * up a `ctx.props` the provider did not set.
26
26
  */
27
- export declare function serveAnonymous(request: Request, env: Env): Promise<Response>;
27
+ export declare function serveAnonymous(request: Request, env: Env, hosts?: readonly string[]): Promise<Response>;
28
28
  export declare const mcpApiHandler: {
29
29
  fetch(request: Request, env: Env, ctx: AuthedContext): Promise<Response>;
30
30
  };
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-api-handler.d.ts","sourceRoot":"","sources":["../../src/auth/mcp-api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAGvC,KAAK,aAAa,GAAG,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAA;AAEzD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAE5E;AAED,eAAO,MAAM,aAAa;mBACH,OAAO,OAAO,GAAG,OAAO,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC;CA4B/E,CAAA"}
1
+ {"version":3,"file":"mcp-api-handler.d.ts","sourceRoot":"","sources":["../../src/auth/mcp-api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAIvC,KAAK,aAAa,GAAG,gBAAgB,GAAG;IAAE,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAA;AAEzD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,GAAE,SAAS,MAAM,EAA6B,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEjI;AAED,eAAO,MAAM,aAAa;mBACH,OAAO,OAAO,GAAG,OAAO,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC;CA4B/E,CAAA"}
@@ -15,6 +15,7 @@
15
15
  import { createMziziHttpHandler } from "../http.js";
16
16
  import { createFundiCaller, createUserTokenResolver, fundiConfigured } from "../fundi-client.js";
17
17
  import { deskFor } from "../desk-context.js";
18
+ import { allowedHostsFromEnv } from "../origin.js";
18
19
  /**
19
20
  * The free tier: no user. `fundi` says whether to LIST mzizi_fundi (so its
20
21
  * refusal can say how to sign in); it wires no caller and no desk.
@@ -22,8 +23,8 @@ import { deskFor } from "../desk-context.js";
22
23
  * Takes no `ctx` on purpose, so the anonymous front in `worker.ts` cannot pick
23
24
  * up a `ctx.props` the provider did not set.
24
25
  */
25
- export function serveAnonymous(request, env) {
26
- return createMziziHttpHandler({ anonymous: { fundi: fundiConfigured(env) } })(request);
26
+ export function serveAnonymous(request, env, hosts = allowedHostsFromEnv(env)) {
27
+ return createMziziHttpHandler({ anonymous: { fundi: fundiConfigured(env) }, allowedOrigins: hosts })(request);
27
28
  }
28
29
  export const mcpApiHandler = {
29
30
  async fetch(request, env, ctx) {
@@ -47,7 +48,7 @@ export const mcpApiHandler = {
47
48
  // `idFromName` is deterministic, so the same person reaches the same object
48
49
  // across sessions, devices and years; that is what makes the history real.
49
50
  const desk = deskFor(env.ISSUE_DESK.get(env.ISSUE_DESK.idFromName(props.userId)), props, fundi?.resolveUserToken);
50
- return createMziziHttpHandler({ fundi, desk })(request);
51
+ return createMziziHttpHandler({ fundi, desk, allowedOrigins: allowedHostsFromEnv(env) })(request);
51
52
  },
52
53
  };
53
54
  //# sourceMappingURL=mcp-api-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-api-handler.js","sourceRoot":"","sources":["../../src/auth/mcp-api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGhG,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAI5C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,GAAQ;IACvD,OAAO,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;AACxF,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,GAAQ,EAAE,GAAkB;QACxD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;QACvB,kEAAkE;QAClE,IAAI,CAAC,KAAK,EAAE,MAAM;YAAE,OAAO,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAEvD,yEAAyE;QACzE,4EAA4E;QAC5E,sCAAsC;QACtC,2EAA2E;QAC3E,2EAA2E;QAC3E,sEAAsE;QACtE,4EAA4E;QAC5E,gCAAgC;QAChC,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC;YAChC,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,gBAAgB,EAAE,uBAAuB,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE;YAC3F,CAAC,CAAC,SAAS,CAAA;QACb,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,2EAA2E;QAC3E,MAAM,IAAI,GAAG,OAAO,CAClB,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAC3D,KAAK,EACL,KAAK,EAAE,gBAAgB,CACxB,CAAA;QAED,OAAO,sBAAsB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACzD,CAAC;CACF,CAAA"}
1
+ {"version":3,"file":"mcp-api-handler.js","sourceRoot":"","sources":["../../src/auth/mcp-api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGhG,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAIlD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,GAAQ,EAAE,QAA2B,mBAAmB,CAAC,GAAG,CAAC;IAC5G,OAAO,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;AAC/G,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,GAAQ,EAAE,GAAkB;QACxD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAA;QACvB,kEAAkE;QAClE,IAAI,CAAC,KAAK,EAAE,MAAM;YAAE,OAAO,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAEvD,yEAAyE;QACzE,4EAA4E;QAC5E,sCAAsC;QACtC,2EAA2E;QAC3E,2EAA2E;QAC3E,sEAAsE;QACtE,4EAA4E;QAC5E,gCAAgC;QAChC,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC;YAChC,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,gBAAgB,EAAE,uBAAuB,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE;YAC3F,CAAC,CAAC,SAAS,CAAA;QACb,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,2EAA2E;QAC3E,MAAM,IAAI,GAAG,OAAO,CAClB,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAC3D,KAAK,EACL,KAAK,EAAE,gBAAgB,CACxB,CAAA;QAED,OAAO,sBAAsB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACnG,CAAC;CACF,CAAA"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The OAuth provider's static options, shared by `worker.ts` and the tests that
3
+ * drive the real provider (`test/oauth-provider.test.ts`), so a test cannot
4
+ * pass against a configuration the Worker does not run.
5
+ *
6
+ * Its own module, not `worker.ts`: the Worker's entry module may export only
7
+ * handlers and Durable Object classes.
8
+ */
9
+ export declare const PROVIDER_OPTIONS: {
10
+ readonly apiRoute: "/mcp";
11
+ readonly authorizeEndpoint: "/authorize";
12
+ readonly tokenEndpoint: "/token";
13
+ readonly clientRegistrationEndpoint: "/register";
14
+ readonly allowPlainPKCE: false;
15
+ };
16
+ //# sourceMappingURL=provider-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.d.ts","sourceRoot":"","sources":["../../src/auth/provider-options.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,gBAAgB;;;;;;CAWnB,CAAA"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The OAuth provider's static options, shared by `worker.ts` and the tests that
3
+ * drive the real provider (`test/oauth-provider.test.ts`), so a test cannot
4
+ * pass against a configuration the Worker does not run.
5
+ *
6
+ * Its own module, not `worker.ts`: the Worker's entry module may export only
7
+ * handlers and Durable Object classes.
8
+ */
9
+ export const PROVIDER_OPTIONS = {
10
+ // A prefix: it covers `/mcp` and `/mcp/signin`.
11
+ apiRoute: "/mcp",
12
+ authorizeEndpoint: "/authorize",
13
+ tokenEndpoint: "/token",
14
+ clientRegistrationEndpoint: "/register",
15
+ // OAuth 2.1 and the MCP authorization spec require S256; `plain` offers no
16
+ // protection against an intercepted code (#295). With this `false` the
17
+ // authorization-server metadata advertises `["S256"]` and the token endpoint
18
+ // refuses a `plain` challenge.
19
+ allowPlainPKCE: false,
20
+ };
21
+ //# sourceMappingURL=provider-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-options.js","sourceRoot":"","sources":["../../src/auth/provider-options.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,gDAAgD;IAChD,QAAQ,EAAE,MAAM;IAChB,iBAAiB,EAAE,YAAY;IAC/B,aAAa,EAAE,QAAQ;IACvB,0BAA0B,EAAE,WAAW;IACvC,2EAA2E;IAC3E,uEAAuE;IACvE,6EAA6E;IAC7E,+BAA+B;IAC/B,cAAc,EAAE,KAAK;CACb,CAAA"}
package/dist/env.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface Env {
19
19
  FUNDI_SERVICE?: Fetcher;
20
20
  COOKIE_ENCRYPTION_KEY: string;
21
21
  ISSUE_DESK: DurableObjectNamespace<import("./issue-desk.js").IssueDesk>;
22
+ MZIZI_ALLOWED_ORIGINS?: string;
22
23
  ANON_RATE_LIMIT?: import("./rate-limit.js").RateLimiter;
23
24
  OAUTH_KV: KVNamespace;
24
25
  OAUTH_PROVIDER: OAuthHelpers;
package/dist/env.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAEtE,MAAM,WAAW,GAAG;IAiBlB,gBAAgB,EAAE,MAAM,CAAA;IAYxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAO9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAUlB,aAAa,CAAC,EAAE,OAAO,CAAA;IAGvB,qBAAqB,EAAE,MAAM,CAAA;IAM7B,UAAU,EAAE,sBAAsB,CAAC,OAAO,iBAAiB,EAAE,SAAS,CAAC,CAAA;IAMvE,eAAe,CAAC,EAAE,OAAO,iBAAiB,EAAE,WAAW,CAAA;IAGvD,QAAQ,EAAE,WAAW,CAAA;IACrB,cAAc,EAAE,YAAY,CAAA;CAC7B"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAA;AAEtE,MAAM,WAAW,GAAG;IAiBlB,gBAAgB,EAAE,MAAM,CAAA;IAYxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAO9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAUlB,aAAa,CAAC,EAAE,OAAO,CAAA;IAGvB,qBAAqB,EAAE,MAAM,CAAA;IAM7B,UAAU,EAAE,sBAAsB,CAAC,OAAO,iBAAiB,EAAE,SAAS,CAAC,CAAA;IAMvE,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAM9B,eAAe,CAAC,EAAE,OAAO,iBAAiB,EAAE,WAAW,CAAA;IAGvD,QAAQ,EAAE,WAAW,CAAA;IACrB,cAAc,EAAE,YAAY,CAAA;CAC7B"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The Bundu floor, as this MCP states it.
3
+ *
4
+ * Nothing here is a number. The registry carries the floors in `/v1/brand`
5
+ * `accessibility` (`minTouchTarget`, `minInset`, `minControlGap`, `minLabelGap`,
6
+ * `minItemGap`, `minBodyLeading`), built from `BUNDU_FLOOR` in its
7
+ * `lib/tokens/roles.source.ts` and held equal to it by a registry test. This module reads
8
+ * those keys out of the bundled brand payload, so the MCP serves whatever the pinned
9
+ * registry commit says and cannot drift from it.
10
+ *
11
+ * A pin from before the spacing floor (owner, 2026-11-04, mzizi-registry#617) has no such
12
+ * keys: `spacingFloorFrom` returns undefined then, and the tools say so rather than
13
+ * answering from a copy.
14
+ */
15
+ export type SpacingFloor = {
16
+ /** Padding between text and its container edge, px (`--inset-floor`). */
17
+ insetPx: number;
18
+ /** Gap between adjacent interactive controls, visible edge to visible edge, px (`--gap-floor`). */
19
+ controlGapPx: number;
20
+ /** Gap between a label and its field, px (`--gap-floor`). */
21
+ labelGapPx: number;
22
+ /** Gap between sibling items in a list, grid or toolbar, px (`--gap-floor`). */
23
+ itemGapPx: number;
24
+ /** Body line height, unitless (`--leading-floor`). */
25
+ leadingBody: number;
26
+ };
27
+ /** The CSS custom property that carries each floor. Names, not values: the registry's token layer defines them. */
28
+ export declare const SPACING_FLOOR_PROPERTIES: Record<keyof SpacingFloor, string>;
29
+ /** The spacing floor in a `/v1/brand` payload, or undefined when the pinned registry predates it. */
30
+ export declare function spacingFloorFrom(brand: Record<string, unknown>): SpacingFloor | undefined;
31
+ /**
32
+ * The floor in a phrase for static text (tool descriptions, server instructions), from the
33
+ * pinned registry's numbers at build time. Without them the phrase names no number.
34
+ */
35
+ export declare function spacingFloorPhrase(floor: SpacingFloor | null): string;
36
+ export declare const SPACING_FLOOR_MISSING = "This build's registry pin predates the spacing floor (mzizi-registry#617), so it has no spacing floor numbers to state. Bump the pin in registry.pin.json.";
37
+ /** How the floor is enforced and what to do about it: the same text on every surface that states it. */
38
+ export declare const SPACING_FLOOR_GUIDANCE: {
39
+ readonly meaning: {
40
+ readonly insetPx: "Padding between text and the edge of its container.";
41
+ readonly controlGapPx: "Gap between adjacent interactive controls, visible edge to visible edge. Hit regions may touch, never overlap.";
42
+ readonly labelGapPx: "Gap between a label and its field.";
43
+ readonly itemGapPx: "Gap between sibling items in a list, grid or toolbar.";
44
+ readonly leadingBody: "Body line height (WCAG 1.4.12). Headings keep their own leading.";
45
+ };
46
+ readonly enforcement: "A base-layer rule in the registry's token layer gives every element the floor even when you add no padding or gap; the token test fails a component spec below it; each component contract asserts its spacing minimums (TypeScript and Rust agree); a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress; `mzizi check` (@nyuchi/mzizi-cli 0.8.0 or later) fails a project's CSS or TSX that goes under it, and `mzizi_check_accessibility` checks values you pass it.";
47
+ readonly nothingGoesUnder: "No brand pack, density layer, component or app may lower a floor. `data-mzizi-sizes=\"legacy\"` on the root is the one opt-out, and it steps out of the other Bundu floors too.";
48
+ readonly whatToDo: "Use the components and the tokens (--inset-floor, --gap-floor, --leading-floor). Never write p-0, gap-0, a negative margin or leading-none on a text container or a row of controls without an exception comment: `mzizi-spacing-exempt: <reason>`.";
49
+ };
50
+ /** The floors of a registry, for the catalogue, `mzizi_mcp_describe` and `mzizi://tools`. */
51
+ export declare function floorsOf(registry: () => Promise<{
52
+ getBrand(): {
53
+ data: Record<string, unknown>;
54
+ };
55
+ }>): Promise<{
56
+ spacing: null;
57
+ hitAreaPx: number | null;
58
+ note: string;
59
+ } | {
60
+ source: string;
61
+ meaning: {
62
+ readonly insetPx: "Padding between text and the edge of its container.";
63
+ readonly controlGapPx: "Gap between adjacent interactive controls, visible edge to visible edge. Hit regions may touch, never overlap.";
64
+ readonly labelGapPx: "Gap between a label and its field.";
65
+ readonly itemGapPx: "Gap between sibling items in a list, grid or toolbar.";
66
+ readonly leadingBody: "Body line height (WCAG 1.4.12). Headings keep their own leading.";
67
+ };
68
+ enforcement: "A base-layer rule in the registry's token layer gives every element the floor even when you add no padding or gap; the token test fails a component spec below it; each component contract asserts its spacing minimums (TypeScript and Rust agree); a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress; `mzizi check` (@nyuchi/mzizi-cli 0.8.0 or later) fails a project's CSS or TSX that goes under it, and `mzizi_check_accessibility` checks values you pass it.";
69
+ nothingGoesUnder: "No brand pack, density layer, component or app may lower a floor. `data-mzizi-sizes=\"legacy\"` on the root is the one opt-out, and it steps out of the other Bundu floors too.";
70
+ whatToDo: "Use the components and the tokens (--inset-floor, --gap-floor, --leading-floor). Never write p-0, gap-0, a negative margin or leading-none on a text container or a row of controls without an exception comment: `mzizi-spacing-exempt: <reason>`.";
71
+ spacing: SpacingFloor;
72
+ customProperties: Record<keyof SpacingFloor, string>;
73
+ hitAreaPx: number | null;
74
+ note?: undefined;
75
+ }>;
76
+ /** The floors as `mzizi_get_tokens`, `mzizi_get_doctrine` and `mzizi_mcp_describe` state them. */
77
+ export declare function describeFloors(brand: Record<string, unknown>): {
78
+ spacing: null;
79
+ hitAreaPx: number | null;
80
+ note: string;
81
+ } | {
82
+ source: string;
83
+ meaning: {
84
+ readonly insetPx: "Padding between text and the edge of its container.";
85
+ readonly controlGapPx: "Gap between adjacent interactive controls, visible edge to visible edge. Hit regions may touch, never overlap.";
86
+ readonly labelGapPx: "Gap between a label and its field.";
87
+ readonly itemGapPx: "Gap between sibling items in a list, grid or toolbar.";
88
+ readonly leadingBody: "Body line height (WCAG 1.4.12). Headings keep their own leading.";
89
+ };
90
+ enforcement: "A base-layer rule in the registry's token layer gives every element the floor even when you add no padding or gap; the token test fails a component spec below it; each component contract asserts its spacing minimums (TypeScript and Rust agree); a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress; `mzizi check` (@nyuchi/mzizi-cli 0.8.0 or later) fails a project's CSS or TSX that goes under it, and `mzizi_check_accessibility` checks values you pass it.";
91
+ nothingGoesUnder: "No brand pack, density layer, component or app may lower a floor. `data-mzizi-sizes=\"legacy\"` on the root is the one opt-out, and it steps out of the other Bundu floors too.";
92
+ whatToDo: "Use the components and the tokens (--inset-floor, --gap-floor, --leading-floor). Never write p-0, gap-0, a negative margin or leading-none on a text container or a row of controls without an exception comment: `mzizi-spacing-exempt: <reason>`.";
93
+ spacing: SpacingFloor;
94
+ customProperties: Record<keyof SpacingFloor, string>;
95
+ hitAreaPx: number | null;
96
+ note?: undefined;
97
+ };
98
+ export type SpacingChecks = {
99
+ insetPx?: number;
100
+ controlGapPx?: number;
101
+ labelGapPx?: number;
102
+ itemGapPx?: number;
103
+ leading?: number;
104
+ };
105
+ /** Compare the values a caller states with the floor. Only the values given are checked. */
106
+ export declare function checkSpacing(floor: SpacingFloor, given: SpacingChecks): {
107
+ floor: SpacingFloor;
108
+ checks: {
109
+ name: string;
110
+ value: number;
111
+ minimum: number;
112
+ unit: "px" | "ratio";
113
+ passes: boolean;
114
+ }[];
115
+ meetsFloor: boolean;
116
+ };
117
+ //# sourceMappingURL=floors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floors.d.ts","sourceRoot":"","sources":["../src/floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,YAAY,GAAG;IACzB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,YAAY,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,mHAAmH;AACnH,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,YAAY,EAAE,MAAM,CAMvE,CAAC;AAYF,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,GAAG,SAAS,CAWzF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,GAAG,MAAM,CAGrE;AAED,eAAO,MAAM,qBAAqB,+JAC4H,CAAC;AAE/J,wGAAwG;AACxG,eAAO,MAAM,sBAAsB;;;;;;;;;;;CAezB,CAAC;AAEX,6FAA6F;AAC7F,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC;IAAE,QAAQ,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;;;GAExG;AAED,kGAAkG;AAClG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;EAe5D;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa;;;cAC1C,MAAM;eAAS,MAAM;iBAAW,MAAM;cAAQ,IAAI,GAAG,OAAO;gBAAU,OAAO;;;EAUxG"}
package/dist/floors.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The Bundu floor, as this MCP states it.
3
+ *
4
+ * Nothing here is a number. The registry carries the floors in `/v1/brand`
5
+ * `accessibility` (`minTouchTarget`, `minInset`, `minControlGap`, `minLabelGap`,
6
+ * `minItemGap`, `minBodyLeading`), built from `BUNDU_FLOOR` in its
7
+ * `lib/tokens/roles.source.ts` and held equal to it by a registry test. This module reads
8
+ * those keys out of the bundled brand payload, so the MCP serves whatever the pinned
9
+ * registry commit says and cannot drift from it.
10
+ *
11
+ * A pin from before the spacing floor (owner, 2026-11-04, mzizi-registry#617) has no such
12
+ * keys: `spacingFloorFrom` returns undefined then, and the tools say so rather than
13
+ * answering from a copy.
14
+ */
15
+ /** The CSS custom property that carries each floor. Names, not values: the registry's token layer defines them. */
16
+ export const SPACING_FLOOR_PROPERTIES = {
17
+ insetPx: "--inset-floor",
18
+ controlGapPx: "--gap-floor",
19
+ labelGapPx: "--gap-floor",
20
+ itemGapPx: "--gap-floor",
21
+ leadingBody: "--leading-floor",
22
+ };
23
+ const KEYS = {
24
+ insetPx: "minInset",
25
+ controlGapPx: "minControlGap",
26
+ labelGapPx: "minLabelGap",
27
+ itemGapPx: "minItemGap",
28
+ leadingBody: "minBodyLeading",
29
+ };
30
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
31
+ /** The spacing floor in a `/v1/brand` payload, or undefined when the pinned registry predates it. */
32
+ export function spacingFloorFrom(brand) {
33
+ const a = brand.accessibility;
34
+ if (!a || typeof a !== "object")
35
+ return undefined;
36
+ const rec = a;
37
+ const out = {};
38
+ for (const [field, key] of Object.entries(KEYS)) {
39
+ const v = num(rec[key]);
40
+ if (v === undefined)
41
+ return undefined;
42
+ out[field] = v;
43
+ }
44
+ return out;
45
+ }
46
+ /**
47
+ * The floor in a phrase for static text (tool descriptions, server instructions), from the
48
+ * pinned registry's numbers at build time. Without them the phrase names no number.
49
+ */
50
+ export function spacingFloorPhrase(floor) {
51
+ if (!floor)
52
+ return "the spacing floor (inset, gaps between controls, labels and items, and body leading)";
53
+ return `the spacing floor (${floor.insetPx}px inset, ${floor.controlGapPx}px gaps between controls, ${floor.labelGapPx}px between a label and its field, ${floor.itemGapPx}px between items, ${floor.leadingBody} body leading)`;
54
+ }
55
+ export const SPACING_FLOOR_MISSING = "This build's registry pin predates the spacing floor (mzizi-registry#617), so it has no spacing floor numbers to state. Bump the pin in registry.pin.json.";
56
+ /** How the floor is enforced and what to do about it: the same text on every surface that states it. */
57
+ export const SPACING_FLOOR_GUIDANCE = {
58
+ meaning: {
59
+ insetPx: "Padding between text and the edge of its container.",
60
+ controlGapPx: "Gap between adjacent interactive controls, visible edge to visible edge. Hit regions may touch, never overlap.",
61
+ labelGapPx: "Gap between a label and its field.",
62
+ itemGapPx: "Gap between sibling items in a list, grid or toolbar.",
63
+ leadingBody: "Body line height (WCAG 1.4.12). Headings keep their own leading.",
64
+ },
65
+ enforcement: "A base-layer rule in the registry's token layer gives every element the floor even when you add no padding or gap; the token test fails a component spec below it; each component contract asserts its spacing minimums (TypeScript and Rust agree); a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress; `mzizi check` (@nyuchi/mzizi-cli 0.8.0 or later) fails a project's CSS or TSX that goes under it, and `mzizi_check_accessibility` checks values you pass it.",
66
+ nothingGoesUnder: "No brand pack, density layer, component or app may lower a floor. `data-mzizi-sizes=\"legacy\"` on the root is the one opt-out, and it steps out of the other Bundu floors too.",
67
+ whatToDo: "Use the components and the tokens (--inset-floor, --gap-floor, --leading-floor). Never write p-0, gap-0, a negative margin or leading-none on a text container or a row of controls without an exception comment: `mzizi-spacing-exempt: <reason>`.",
68
+ };
69
+ /** The floors of a registry, for the catalogue, `mzizi_mcp_describe` and `mzizi://tools`. */
70
+ export async function floorsOf(registry) {
71
+ return describeFloors((await registry()).getBrand().data);
72
+ }
73
+ /** The floors as `mzizi_get_tokens`, `mzizi_get_doctrine` and `mzizi_mcp_describe` state them. */
74
+ export function describeFloors(brand) {
75
+ const spacing = spacingFloorFrom(brand);
76
+ const a = (brand.accessibility ?? {});
77
+ const hitAreaPx = num(a.minTouchTarget);
78
+ if (!spacing) {
79
+ return { spacing: null, hitAreaPx: hitAreaPx ?? null, note: SPACING_FLOOR_MISSING };
80
+ }
81
+ return {
82
+ spacing,
83
+ customProperties: SPACING_FLOOR_PROPERTIES,
84
+ hitAreaPx: hitAreaPx ?? null,
85
+ ...SPACING_FLOOR_GUIDANCE,
86
+ source: "mzizi_get_tokens family=accessibility (brand.accessibility at the pinned registry commit; BUNDU_FLOOR.spacing in lib/tokens/roles.source.ts)",
87
+ };
88
+ }
89
+ /** Compare the values a caller states with the floor. Only the values given are checked. */
90
+ export function checkSpacing(floor, given) {
91
+ const rows = [];
92
+ const add = (name, value, minimum, unit) => {
93
+ if (value !== undefined)
94
+ rows.push({ name, value, minimum, unit, passes: value >= minimum });
95
+ };
96
+ add("inset", given.insetPx, floor.insetPx, "px");
97
+ add("controlGap", given.controlGapPx, floor.controlGapPx, "px");
98
+ add("labelGap", given.labelGapPx, floor.labelGapPx, "px");
99
+ add("itemGap", given.itemGapPx, floor.itemGapPx, "px");
100
+ add("leading", given.leading, floor.leadingBody, "ratio");
101
+ return { floor, checks: rows, meetsFloor: rows.every((r) => r.passes) };
102
+ }
103
+ //# sourceMappingURL=floors.js.map