@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.
- package/README.md +105 -39
- package/dist/auth/front.d.ts +8 -0
- package/dist/auth/front.d.ts.map +1 -1
- package/dist/auth/front.js +10 -0
- package/dist/auth/front.js.map +1 -1
- package/dist/auth/mcp-api-handler.d.ts +1 -1
- package/dist/auth/mcp-api-handler.d.ts.map +1 -1
- package/dist/auth/mcp-api-handler.js +4 -3
- package/dist/auth/mcp-api-handler.js.map +1 -1
- package/dist/auth/provider-options.d.ts +16 -0
- package/dist/auth/provider-options.d.ts.map +1 -0
- package/dist/auth/provider-options.js +21 -0
- package/dist/auth/provider-options.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/floors.d.ts +117 -0
- package/dist/floors.d.ts.map +1 -0
- package/dist/floors.js +103 -0
- package/dist/floors.js.map +1 -0
- package/dist/generated/registry-pin.d.ts +8 -1
- package/dist/generated/registry-pin.d.ts.map +1 -1
- package/dist/generated/registry-pin.js +3 -1
- package/dist/generated/registry-pin.js.map +1 -1
- package/dist/generated/registry.d.ts +2 -2
- package/dist/generated/registry.js +198 -198
- package/dist/generated/registry.js.map +1 -1
- package/dist/generated/skills.d.ts +1 -1
- package/dist/generated/skills.js +13 -13
- package/dist/generated/skills.js.map +1 -1
- package/dist/http.d.ts +14 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +38 -54
- package/dist/http.js.map +1 -1
- package/dist/origin.d.ts +55 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +127 -0
- package/dist/origin.js.map +1 -0
- package/dist/rate-limit.d.ts +1 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +3 -2
- package/dist/rate-limit.js.map +1 -1
- package/dist/server.d.ts +47 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -8
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +115 -48
- package/dist/tools.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +19 -11
- package/dist/worker.js.map +1 -1
- 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.
|
|
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,
|
|
76
|
-
|
|
77
|
-
[docs.mzizi.dev/toolchain/connect](https://docs.mzizi.dev/toolchain/connect)
|
|
78
|
-
|
|
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
|
|
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
|
|
108
|
-
- **
|
|
109
|
-
|
|
110
|
-
`"
|
|
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
|
|
124
|
-
`.agents/skills/`, where `npx skills experimental_sync` links the npm
|
|
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`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
374
|
-
|
|
|
375
|
-
| `WORKOS_CLIENT_ID`
|
|
376
|
-
| `WORKOS_M2M_CLIENT_ID`
|
|
377
|
-
| `FUNDI_URL`
|
|
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),
|
package/dist/auth/front.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/auth/front.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/auth/front.js
CHANGED
|
@@ -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
|
*
|
package/dist/auth/front.js.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|
package/dist/floors.d.ts
ADDED
|
@@ -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
|