@nyuchi/mzizi-mcp 0.13.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 +197 -26
- 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/docs-federation.d.ts +1 -1
- package/dist/docs-federation.d.ts.map +1 -1
- package/dist/docs-federation.js +1 -0
- package/dist/docs-federation.js.map +1 -1
- 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/fundi-client.d.ts +6 -0
- package/dist/fundi-client.d.ts.map +1 -1
- package/dist/fundi-client.js +14 -3
- package/dist/fundi-client.js.map +1 -1
- 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.d.ts.map +1 -1
- package/dist/generated/registry.js +381 -325
- package/dist/generated/registry.js.map +1 -1
- package/dist/generated/skills.d.ts +11 -1
- package/dist/generated/skills.d.ts.map +1 -1
- package/dist/generated/skills.js +216 -21
- package/dist/generated/skills.js.map +1 -1
- package/dist/http.d.ts +32 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +109 -28
- package/dist/http.js.map +1 -1
- package/dist/limits.d.ts +32 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +44 -0
- package/dist/limits.js.map +1 -0
- 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/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +22 -1
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +83 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +109 -10
- package/dist/server.js.map +1 -1
- package/dist/stdio.js +5 -4
- package/dist/stdio.js.map +1 -1
- package/dist/tools.d.ts +21 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +413 -114
- package/dist/tools.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +19 -8
- package/dist/worker.js.map +1 -1
- package/package.json +18 -17
package/dist/tools.js
CHANGED
|
@@ -25,7 +25,24 @@ import { bundledRegistry } from "./registry.js";
|
|
|
25
25
|
import { dispatchFundiA2A, FUNDI_SUBMIT_TOOL, FUNDI_STATUS_TOOL, FUNDI_CANCEL_TOOL, } from "./fundi-a2a.js";
|
|
26
26
|
import { jsonContent, toolError } from "./tool-result.js";
|
|
27
27
|
import { signInRequired } from "./access.js";
|
|
28
|
+
import { CHARACTER_LIMIT, fitToLimit } from "./limits.js";
|
|
29
|
+
import { REGISTRY_SPACING_FLOOR } from "./generated/registry-pin.js";
|
|
30
|
+
import { SPACING_FLOOR_MISSING, checkSpacing, describeFloors, spacingFloorFrom, spacingFloorPhrase } from "./floors.js";
|
|
31
|
+
/** The spacing floor in words, from the pinned registry's numbers (no number when the pin predates it). */
|
|
32
|
+
const SPACING_PHRASE = spacingFloorPhrase(REGISTRY_SPACING_FLOOR);
|
|
28
33
|
import { BUNDLED_SKILLS, SKILL_CATEGORIES, SKILLS_VERSION } from "./generated/skills.js";
|
|
34
|
+
/** The MCP resource URI of a skill's SKILL.md body, and of a file it ships. */
|
|
35
|
+
export const skillUri = (name) => `mzizi://skills/${name}`;
|
|
36
|
+
export const skillFileUri = (name, path) => `mzizi://skills/${name}/${path}`;
|
|
37
|
+
/** A skill as the tool returns it: the body in full, its files as a list (content on request). */
|
|
38
|
+
export function skillView(skill) {
|
|
39
|
+
const { files, ...rest } = skill;
|
|
40
|
+
return {
|
|
41
|
+
...rest,
|
|
42
|
+
uri: skillUri(skill.name),
|
|
43
|
+
files: files.map((f) => ({ path: f.path, mimeType: f.mimeType, bytes: f.bytes, encoding: f.encoding, uri: skillFileUri(skill.name, f.path) })),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
29
46
|
export const TOOL_PREFIX = "mzizi_";
|
|
30
47
|
/** Unwrap a registry result into an MCP tool result, keeping the failure readable. */
|
|
31
48
|
function respond(result, wrapKey) {
|
|
@@ -40,6 +57,7 @@ function respond(result, wrapKey) {
|
|
|
40
57
|
*/
|
|
41
58
|
const READ = {
|
|
42
59
|
readOnlyHint: true,
|
|
60
|
+
destructiveHint: false,
|
|
43
61
|
idempotentHint: true,
|
|
44
62
|
openWorldHint: false,
|
|
45
63
|
};
|
|
@@ -60,6 +78,9 @@ const READ = {
|
|
|
60
78
|
* are always reported, so nothing is hidden and nothing is silently truncated.
|
|
61
79
|
*/
|
|
62
80
|
const LIST_DEFAULT_LIMIT = 400;
|
|
81
|
+
/** Default and maximum hits per `search` call (the default was the registry's own 50). */
|
|
82
|
+
const SEARCH_DEFAULT_LIMIT = 50;
|
|
83
|
+
const SEARCH_MAX_LIMIT = 200;
|
|
63
84
|
/**
|
|
64
85
|
* A number argument that also accepts the string form of itself.
|
|
65
86
|
*
|
|
@@ -180,13 +201,135 @@ function astroSection(doc) {
|
|
|
180
201
|
files: doc.files.map((f) => f.encoding === "base64" ? { path: f.path, type: f.type, target: f.target, encoding: f.encoding } : f),
|
|
181
202
|
};
|
|
182
203
|
}
|
|
183
|
-
/**
|
|
184
|
-
function
|
|
204
|
+
/** Lowercase hex SHA-256 of a UTF-8 string. Web Crypto, so it runs in the Worker and the stdio bin. */
|
|
205
|
+
async function sha256Hex(text) {
|
|
206
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
207
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The source with its `//` line and `/* *\/` block comments removed, so an import
|
|
211
|
+
* written in a comment is not read as one. String and template literals are copied
|
|
212
|
+
* through untouched, so a `//` inside `"https://…"` does not start a comment.
|
|
213
|
+
*/
|
|
214
|
+
function stripComments(src) {
|
|
215
|
+
let out = "";
|
|
216
|
+
let i = 0;
|
|
217
|
+
while (i < src.length) {
|
|
218
|
+
const c = src[i];
|
|
219
|
+
const next = src[i + 1];
|
|
220
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
221
|
+
let j = i + 1;
|
|
222
|
+
while (j < src.length && src[j] !== c)
|
|
223
|
+
j += src[j] === "\\" ? 2 : 1;
|
|
224
|
+
out += src.slice(i, j + 1);
|
|
225
|
+
i = j + 1;
|
|
226
|
+
}
|
|
227
|
+
else if (c === "/" && next === "/") {
|
|
228
|
+
const end = src.indexOf("\n", i);
|
|
229
|
+
i = end === -1 ? src.length : end;
|
|
230
|
+
}
|
|
231
|
+
else if (c === "/" && next === "*") {
|
|
232
|
+
const end = src.indexOf("*/", i + 2);
|
|
233
|
+
// Keep a space so the tokens either side of the comment stay apart.
|
|
234
|
+
out += " ";
|
|
235
|
+
i = end === -1 ? src.length : end + 2;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
out += c;
|
|
239
|
+
i += 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Every `@/…` module specifier a source file imports, re-exports or loads with a
|
|
246
|
+
* dynamic `import("@/…")`, in first-seen order. Comments are ignored.
|
|
247
|
+
*/
|
|
248
|
+
function aliasImports(content) {
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
const code = stripComments(content);
|
|
251
|
+
for (const m of code.matchAll(/(?:\bfrom|\bimport)\s*\(?\s*["'`](@\/[^"'`]+)["'`]/g)) {
|
|
252
|
+
if (m[1])
|
|
253
|
+
seen.add(m[1]);
|
|
254
|
+
}
|
|
255
|
+
return [...seen];
|
|
256
|
+
}
|
|
257
|
+
/** The `@/…` specifier a file installed at `target` answers (`lib/icons/index.tsx` → `@/lib/icons`). */
|
|
258
|
+
function aliasOfTarget(target) {
|
|
259
|
+
return "@/" + target.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "");
|
|
260
|
+
}
|
|
261
|
+
const UI_PREFIX = "https://api.mzizi.dev/v1/ui/";
|
|
262
|
+
/**
|
|
263
|
+
* The raw file at exactly `ref`: `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`.
|
|
264
|
+
* Null when the registry is not a GitHub repository or the file has no path.
|
|
265
|
+
*/
|
|
266
|
+
function pinnedFileUrl(repository, ref, path) {
|
|
267
|
+
const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(repository);
|
|
268
|
+
if (!m || !path)
|
|
269
|
+
return null;
|
|
270
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
271
|
+
return `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${ref}/${encoded}`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* What a consumer needs to vendor the React build without calling the API
|
|
275
|
+
* (mzizi-registry#532): the registry commit this answer comes from, each file's
|
|
276
|
+
* install target, SHA-256 and a URL pinned to that commit, each `@/` import with
|
|
277
|
+
* the declared registry dependency that installs it (or null when none does),
|
|
278
|
+
* and, labelled as live, the API item URL, which serves whatever commit the API
|
|
279
|
+
* is pinned to and can differ from `registry.ref`.
|
|
280
|
+
*/
|
|
281
|
+
async function vendorSection(body, r) {
|
|
282
|
+
const name = String(body.name ?? "");
|
|
283
|
+
const files = (Array.isArray(body.files) ? body.files : []);
|
|
284
|
+
const deps = (Array.isArray(body.registryDependencies) ? body.registryDependencies : []);
|
|
285
|
+
// The alias each declared dependency's files answer, so an import can be matched to its installer.
|
|
286
|
+
const provides = new Map();
|
|
287
|
+
for (const dep of deps) {
|
|
288
|
+
if (!dep.startsWith(UI_PREFIX))
|
|
289
|
+
continue;
|
|
290
|
+
const depName = dep.slice(UI_PREFIX.length);
|
|
291
|
+
const got = r.getComponent(depName);
|
|
292
|
+
if (!got.ok)
|
|
293
|
+
continue;
|
|
294
|
+
for (const f of (got.data.files ?? [])) {
|
|
295
|
+
if (f.target)
|
|
296
|
+
provides.set(aliasOfTarget(f.target), depName);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const own = new Set(files.flatMap((f) => (f.target ? [aliasOfTarget(f.target)] : [])));
|
|
300
|
+
const imports = [...new Set(files.flatMap((f) => aliasImports(f.content ?? "")))]
|
|
301
|
+
.filter((spec) => !own.has(spec))
|
|
302
|
+
.map((spec) => ({ alias: spec, providedBy: provides.get(spec) ?? null }));
|
|
303
|
+
return {
|
|
304
|
+
registry: { repository: r.pin.repository, ref: r.pin.ref },
|
|
305
|
+
files: await Promise.all(files.map(async (f) => ({
|
|
306
|
+
path: f.path ?? null,
|
|
307
|
+
target: f.target ?? null,
|
|
308
|
+
url: pinnedFileUrl(r.pin.repository, r.pin.ref, f.path),
|
|
309
|
+
sha256: typeof f.content === "string" ? await sha256Hex(f.content) : null,
|
|
310
|
+
}))),
|
|
311
|
+
imports,
|
|
312
|
+
live: {
|
|
313
|
+
url: `${UI_PREFIX}${encodeURIComponent(name)}`,
|
|
314
|
+
note: "Live, not pinned: the API serves the registry commit it is deployed at, which can differ from registry.ref, " +
|
|
315
|
+
"so its files may not match these sha256 values. Use files[].url for the pinned source.",
|
|
316
|
+
},
|
|
317
|
+
note: "Components have no version numbers of their own: pin a vendored copy to registry.ref and check each file's sha256. " +
|
|
318
|
+
"Each sha256 is of the registry source at path@registry.ref (what files[].url serves). A file written by `shadcn add` " +
|
|
319
|
+
"can differ from it: shadcn strips \"use client\" when the project sets rsc: false, and rewrites @/ aliases to the " +
|
|
320
|
+
"project's own, so hash the registry source, not the installed file. " +
|
|
321
|
+
"An import whose providedBy is null is not installed by any declared registry dependency. @/lib/utils is provided " +
|
|
322
|
+
"by the registry's utils item, or by shadcn init in a project that ran it.",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** The React build, second and labelled: the registry's `/v1/ui/{name}` body, plus `vendor` install metadata. */
|
|
326
|
+
async function reactSection(body, r) {
|
|
185
327
|
const name = String(body.name ?? "");
|
|
186
328
|
return {
|
|
187
329
|
build: "react",
|
|
188
330
|
status: "deprioritised",
|
|
189
331
|
install: `npx shadcn@latest add https://api.mzizi.dev/v1/ui/${encodeURIComponent(name)}`,
|
|
332
|
+
vendor: await vendorSection(body, r),
|
|
190
333
|
...body,
|
|
191
334
|
};
|
|
192
335
|
}
|
|
@@ -202,24 +345,68 @@ export function registerMziziTools(server, opts) {
|
|
|
202
345
|
// 1 ─ search
|
|
203
346
|
server.registerTool(note("search"), {
|
|
204
347
|
title: "Search Mzizi",
|
|
205
|
-
description: "Search the registry by words: components, conventions, AI instruction sets and releases. Start here when you do not know the exact name of what you want. A component hit with `rustCrate` has a Mzizi Roots (Rust) implementation, which is the lead; at an equal score it ranks first. Documentation pages are searched by the docs_* tools."
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
348
|
+
description: "Search the registry by words: components, conventions, AI instruction sets and releases. Start here when you do not know the exact name of what you want; use mzizi_get_component once you have a component's name. A component hit with `rustCrate` has a Mzizi Roots (Rust) implementation, which is the lead; at an equal score it ranks first. Documentation pages are searched by the docs_* tools. " +
|
|
349
|
+
`Paged: up to ${SEARCH_DEFAULT_LIMIT} hits per call and at most ${CHARACTER_LIMIT} characters; meta.has_more and meta.next_offset say how to get the rest.`,
|
|
350
|
+
inputSchema: z.object({
|
|
351
|
+
query: z.string().min(1).max(500).describe("Free-text search term, e.g. 'button' or 'date picker'. Every word must match."),
|
|
352
|
+
limit: NUM.int().positive().optional().describe(`Hits per call (default ${SEARCH_DEFAULT_LIMIT}, at most ${SEARCH_MAX_LIMIT}).`),
|
|
353
|
+
offset: NUM.int().min(0).optional().describe("Hits to skip, for paging. Use meta.next_offset from the previous call."),
|
|
354
|
+
}),
|
|
355
|
+
outputSchema: z.object({
|
|
356
|
+
results: z.object({
|
|
357
|
+
data: z.array(z.looseObject({
|
|
358
|
+
kind: z.string(),
|
|
359
|
+
name: z.string(),
|
|
360
|
+
title: z.string().optional(),
|
|
361
|
+
description: z.string().optional(),
|
|
362
|
+
rustCrate: z.string().optional(),
|
|
363
|
+
astroFile: z.string().optional(),
|
|
364
|
+
})),
|
|
365
|
+
meta: z.looseObject({
|
|
366
|
+
total: z.number(),
|
|
367
|
+
count: z.number(),
|
|
368
|
+
offset: z.number(),
|
|
369
|
+
has_more: z.boolean(),
|
|
370
|
+
next_offset: z.number().optional(),
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
}),
|
|
209
374
|
annotations: READ,
|
|
210
|
-
}, async ({ query }) =>
|
|
375
|
+
}, async ({ query, limit, offset }) => {
|
|
376
|
+
const res = (await reg()).search(query, Number.MAX_SAFE_INTEGER);
|
|
377
|
+
const all = res.data.data;
|
|
378
|
+
const skip = offset ?? 0;
|
|
379
|
+
const take = Math.min(limit ?? SEARCH_DEFAULT_LIMIT, SEARCH_MAX_LIMIT);
|
|
380
|
+
const render = (page) => {
|
|
381
|
+
const next = skip + page.length;
|
|
382
|
+
const hasMore = next < all.length;
|
|
383
|
+
return {
|
|
384
|
+
data: page,
|
|
385
|
+
meta: {
|
|
386
|
+
...res.data.meta,
|
|
387
|
+
total: all.length,
|
|
388
|
+
count: page.length,
|
|
389
|
+
offset: skip,
|
|
390
|
+
has_more: hasMore,
|
|
391
|
+
...(hasMore ? { next_offset: next } : {}),
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
const { result } = fitToLimit(all.slice(skip, skip + take), render);
|
|
396
|
+
return jsonContent(result, "results");
|
|
397
|
+
});
|
|
211
398
|
// 2 ─ get_component — one read, everything, Rust first
|
|
212
399
|
server.registerTool(note("get_component"), {
|
|
213
400
|
title: "Get a component",
|
|
214
|
-
description: "Fetch one component in full, Rust first. When the component has a Mzizi Roots (Rust
|
|
401
|
+
description: "Fetch one component in full, Rust first. When the component has a Mzizi Roots (Rust) implementation, `rust` comes first (Roots' crates are built on Dioxus 0.7 today and are moving to Mzizi's own renderer): its crate, the `cargo add` line, its CONTRACT clause block and the `.rs` source inline. `astro` follows when the component has a pure Astro implementation (mzizi-registry#397): its `mzizi add --target astro` line, the files it installs into src/components/mzizi/ and their dependencies; null otherwise. `react` follows: the React build (deprioritised; it keeps working, new work goes to Rust), shadcn-shaped with files[] inline, its `npx shadcn add` line, and `vendor`: the registry commit, each file's install target, sha256 and a raw URL pinned to that commit, each `@/` import with the dependency that installs it, and the live API item URL (labelled live: it can serve a different commit). Each sha256 is of the registry source at path@ref; a file `shadcn add` writes can differ (it strips \"use client\" under rsc: false and rewrites aliases). With no Rust, `rust` is null and `lead` is \"react\". " +
|
|
215
402
|
"Add docs (use cases, variants, a11y: the documented contract) and version history (from the changelog) with `include`. Former nyuchi-* names resolve to their mzizi-* successors.",
|
|
216
|
-
inputSchema: {
|
|
217
|
-
name: z.string().min(1).describe("Component name, e.g. 'button'."),
|
|
403
|
+
inputSchema: z.object({
|
|
404
|
+
name: z.string().max(200).min(1).describe("Component name, e.g. 'button'."),
|
|
218
405
|
include: z
|
|
219
406
|
.array(z.enum(["docs", "versions"]))
|
|
220
407
|
.optional()
|
|
221
408
|
.describe("Extra sections to fetch. Omit for the implementations only."),
|
|
222
|
-
},
|
|
409
|
+
}),
|
|
223
410
|
annotations: READ,
|
|
224
411
|
}, async ({ name, include }) => {
|
|
225
412
|
const r = await reg();
|
|
@@ -237,10 +424,12 @@ export function registerMziziTools(server, opts) {
|
|
|
237
424
|
note: rust.ok
|
|
238
425
|
? `Rust first: "${current}" has a Mzizi Roots implementation in the ${rust.data.crate.name} crate — use it. ` +
|
|
239
426
|
"The React build below is deprioritised: it keeps working, but new work goes to Rust."
|
|
240
|
-
:
|
|
427
|
+
: rust.message,
|
|
241
428
|
rust: rust.ok ? rustSection(rust.data, r.pin.ref) : null,
|
|
242
429
|
astro: astro.ok ? astroSection(astro.data) : null,
|
|
243
|
-
react: react.ok
|
|
430
|
+
react: react.ok
|
|
431
|
+
? await reactSection(react.data, r)
|
|
432
|
+
: { build: "react", error: react.message },
|
|
244
433
|
};
|
|
245
434
|
// A failure in an extra section must not discard the component the caller
|
|
246
435
|
// actually asked for -- it is reported alongside it.
|
|
@@ -259,8 +448,8 @@ export function registerMziziTools(server, opts) {
|
|
|
259
448
|
"Returns a lean row per component (name, type, node, owner, collection, and rustCrate " +
|
|
260
449
|
"when it has a Mzizi Roots (Rust) implementation, which is the lead) — use " +
|
|
261
450
|
"get_component for one component in full, or search when you only have words. " +
|
|
262
|
-
`Paged: up to ${LIST_DEFAULT_LIMIT} rows per call,
|
|
263
|
-
inputSchema: {
|
|
451
|
+
`Paged: up to ${LIST_DEFAULT_LIMIT} rows and at most ${CHARACTER_LIMIT} characters per call; total, has_more and next_offset say how to get the rest.`,
|
|
452
|
+
inputSchema: z.object({
|
|
264
453
|
// No .max(): the node set is uncapped, and a Zod bound would reject a filter
|
|
265
454
|
// before it reached the data, turning a new node into a schema error.
|
|
266
455
|
node: NUM.int()
|
|
@@ -268,15 +457,15 @@ export function registerMziziTools(server, opts) {
|
|
|
268
457
|
.optional()
|
|
269
458
|
.describe("Filter by node number (uncapped)."),
|
|
270
459
|
owner: z
|
|
271
|
-
.string()
|
|
460
|
+
.string().max(100)
|
|
272
461
|
.optional()
|
|
273
462
|
.describe("Filter by owner: mzizi | nyuchi | bundu | framework."),
|
|
274
463
|
collection: z
|
|
275
|
-
.string()
|
|
464
|
+
.string().max(100)
|
|
276
465
|
.optional()
|
|
277
466
|
.describe("Filter by collection, e.g. primitives | brand | pages | resilience."),
|
|
278
467
|
type: z
|
|
279
|
-
.string()
|
|
468
|
+
.string().max(100)
|
|
280
469
|
.optional()
|
|
281
470
|
.describe("Filter by registry type, e.g. registry:ui | registry:lib | registry:block."),
|
|
282
471
|
rust: BOOL.optional().describe("true: only components with a Mzizi Roots (Rust) implementation; false: only those without."),
|
|
@@ -284,12 +473,30 @@ export function registerMziziTools(server, opts) {
|
|
|
284
473
|
limit: NUM.int()
|
|
285
474
|
.positive()
|
|
286
475
|
.optional()
|
|
287
|
-
.describe(`Rows per call (default ${LIST_DEFAULT_LIMIT}).`),
|
|
476
|
+
.describe(`Rows per call (default ${LIST_DEFAULT_LIMIT}). A page also stops at ${CHARACTER_LIMIT} characters.`),
|
|
288
477
|
offset: NUM.int()
|
|
289
478
|
.min(0)
|
|
290
479
|
.optional()
|
|
291
480
|
.describe("Rows to skip, for paging."),
|
|
292
|
-
},
|
|
481
|
+
}),
|
|
482
|
+
outputSchema: z.object({
|
|
483
|
+
count: z.number(),
|
|
484
|
+
total: z.number(),
|
|
485
|
+
offset: z.number(),
|
|
486
|
+
has_more: z.boolean(),
|
|
487
|
+
next_offset: z.number().optional(),
|
|
488
|
+
hasMore: z.boolean(),
|
|
489
|
+
nextOffset: z.number().optional(),
|
|
490
|
+
hint: z.string().optional(),
|
|
491
|
+
registryTotal: z.number().optional(),
|
|
492
|
+
withRust: z.number(),
|
|
493
|
+
withAstro: z.number(),
|
|
494
|
+
items: z.array(z.looseObject({
|
|
495
|
+
name: z.unknown(),
|
|
496
|
+
rustCrate: z.string().optional(),
|
|
497
|
+
astroFile: z.string().optional(),
|
|
498
|
+
})),
|
|
499
|
+
}),
|
|
293
500
|
annotations: READ,
|
|
294
501
|
}, async ({ node, owner, collection, type, rust, astro, limit, offset }) => {
|
|
295
502
|
const r = await reg();
|
|
@@ -329,40 +536,47 @@ export function registerMziziTools(server, opts) {
|
|
|
329
536
|
...(astroFile ? { astroFile } : {}),
|
|
330
537
|
};
|
|
331
538
|
});
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
539
|
+
const render = (page) => {
|
|
540
|
+
const returned = skip + page.length;
|
|
541
|
+
const more = returned < total;
|
|
542
|
+
return {
|
|
543
|
+
count: page.length,
|
|
544
|
+
total,
|
|
545
|
+
offset: skip,
|
|
546
|
+
has_more: more,
|
|
547
|
+
...(more ? { next_offset: returned } : {}),
|
|
548
|
+
// camelCase kept for clients written against 0.13 and earlier.
|
|
549
|
+
hasMore: more,
|
|
550
|
+
...(more
|
|
551
|
+
? {
|
|
552
|
+
nextOffset: returned,
|
|
553
|
+
hint: `Call again with offset=${returned}, or narrow with node/owner/collection.`,
|
|
554
|
+
}
|
|
555
|
+
: {}),
|
|
556
|
+
// Present so a caller can distinguish "your filter matched nothing" from "the
|
|
557
|
+
// registry is empty" — the two look identical in an empty items array, and the
|
|
558
|
+
// first is a query to rewrite while the second is an outage.
|
|
559
|
+
...(registryTotal !== undefined ? { registryTotal } : {}),
|
|
560
|
+
// How many of the `total` matched rows have a Mzizi Roots (Rust)
|
|
561
|
+
// implementation. Filter with rust=true to list only those.
|
|
562
|
+
withRust: payload.meta?.withRust ?? 0,
|
|
563
|
+
// How many have an Astro implementation. Filter with astro=true to list only those.
|
|
564
|
+
withAstro: payload.meta?.withAstro ?? 0,
|
|
565
|
+
items: page,
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
return jsonContent(fitToLimit(items, render).result);
|
|
355
569
|
});
|
|
356
570
|
// 4 ─ get_tokens
|
|
357
571
|
server.registerTool(note("get_tokens"), {
|
|
358
572
|
title: "Get design tokens",
|
|
359
|
-
description: "The Mzizi brand system: 21 colour families (7 minerals, 7 heritage, 7 experimental), semantic colours, typography, spacing, radii and the ecosystem brands. Ask for `colors` to get every colour family in one call.",
|
|
360
|
-
inputSchema: {
|
|
573
|
+
description: "The Mzizi brand system: 21 colour families (7 minerals, 7 heritage, 7 experimental), semantic colours, typography, spacing, radii and the ecosystem brands, and the Bundu floor: the minimums nothing can go under (17px body, 14px captions, 49px hit region, 7:1 body contrast, and " + SPACING_PHRASE + "). Ask for `floors` for the floor in one call (the spacing floor is also under `accessibility`), and for `colors` to get every colour family in one call. For text, use each family's `textLight` / `textDark` on the base surface, and `onContainerLight` / `onContainerDark` on its container; never use `hex`, `lightHex` or `darkHex` as a text colour (they are accents).",
|
|
574
|
+
inputSchema: z.object({
|
|
361
575
|
family: z
|
|
362
|
-
.string()
|
|
576
|
+
.string().max(100)
|
|
363
577
|
.optional()
|
|
364
|
-
.describe("One of: colors (all 21 colour families at once), minerals, heritage, experimental, semantic, backgrounds, typography, spacing, radii, ecosystem, componentSpecs, accessibility, voiceAndTone, philosophy. Common aliases resolve (status/semanticColors, radius, type/fonts, exp). Omit for everything."),
|
|
365
|
-
},
|
|
578
|
+
.describe("One of: colors (all 21 colour families at once), floors (the Bundu floor: sizes, contrast and the spacing floor, with how it is enforced), minerals, heritage, experimental, semantic, backgrounds, typography, spacing (the scale, plus the spacing floor), radii, ecosystem, componentSpecs, accessibility, voiceAndTone, philosophy. Common aliases resolve (status/semanticColors, radius, type/fonts, exp, floor/bundu-floor/spacing-floor). Omit for everything."),
|
|
579
|
+
}),
|
|
366
580
|
annotations: READ,
|
|
367
581
|
}, async ({ family }) => {
|
|
368
582
|
const res = (await reg()).getBrand();
|
|
@@ -441,6 +655,10 @@ export function registerMziziTools(server, opts) {
|
|
|
441
655
|
out[k] = brand[k];
|
|
442
656
|
return jsonContent(out);
|
|
443
657
|
}
|
|
658
|
+
// The Bundu floor in one call, with the numbers the pinned registry states.
|
|
659
|
+
if (["floor", "floors", "bundufloor", "bundufloors", "spacingfloor", "minimums"].includes(asked)) {
|
|
660
|
+
return jsonContent({ floors: describeFloors(brand) });
|
|
661
|
+
}
|
|
444
662
|
const resolved = ALIASES[asked];
|
|
445
663
|
const key = resolved && resolved in brand
|
|
446
664
|
? resolved
|
|
@@ -449,18 +667,25 @@ export function registerMziziTools(server, opts) {
|
|
|
449
667
|
return toolError(`No token family matching "${family}". Available: ${families.join(", ")}. ` +
|
|
450
668
|
`Use "colors" for every colour family at once.`);
|
|
451
669
|
}
|
|
670
|
+
// Asking for the spacing scale also returns the floor under it, so a scale step
|
|
671
|
+
// below the floor is not mistaken for a licence to use it as padding or a gap.
|
|
672
|
+
if (key === "spacing") {
|
|
673
|
+
const spacingFloor = spacingFloorFrom(brand);
|
|
674
|
+
if (spacingFloor)
|
|
675
|
+
return jsonContent({ spacing: brand.spacing, spacingFloor });
|
|
676
|
+
}
|
|
452
677
|
return jsonContent({ [key]: brand[key] });
|
|
453
678
|
});
|
|
454
679
|
// 5 ─ get_architecture
|
|
455
680
|
server.registerTool(note("get_architecture"), {
|
|
456
681
|
title: "Get the architecture",
|
|
457
682
|
description: "The Mzizi DNA double helix — nodes, rungs and strands — with live counts. Pass `node` for one node's covenant, stakeholder and implementation rules.",
|
|
458
|
-
inputSchema: {
|
|
683
|
+
inputSchema: z.object({
|
|
459
684
|
node: NUM.int()
|
|
460
685
|
.positive()
|
|
461
686
|
.optional()
|
|
462
687
|
.describe("One node's detail (uncapped)."),
|
|
463
|
-
},
|
|
688
|
+
}),
|
|
464
689
|
annotations: READ,
|
|
465
690
|
}, async ({ node }) => {
|
|
466
691
|
const r = await reg();
|
|
@@ -469,19 +694,21 @@ export function registerMziziTools(server, opts) {
|
|
|
469
694
|
// 6 ─ get_doctrine
|
|
470
695
|
server.registerTool(note("get_doctrine"), {
|
|
471
696
|
title: "Get doctrine",
|
|
472
|
-
description: "How to build here: Ubuntu pillars and principles, mzizi conventions,
|
|
473
|
-
inputSchema: {
|
|
697
|
+
description: "How to build here: Ubuntu pillars and principles, mzizi conventions, the AI instruction sets, and the Bundu floor (topic `bundu-floor`): the minimums nothing can go under, including " + SPACING_PHRASE + ", how they are enforced and what to do.",
|
|
698
|
+
inputSchema: z.object({
|
|
474
699
|
topic: z
|
|
475
|
-
.enum(["ubuntu-pillars", "ubuntu-principles", "ai-instructions", "conventions"])
|
|
700
|
+
.enum(["ubuntu-pillars", "ubuntu-principles", "ai-instructions", "conventions", "bundu-floor"])
|
|
476
701
|
.describe("Which doctrine to read."),
|
|
477
702
|
name: z
|
|
478
|
-
.string()
|
|
703
|
+
.string().max(200)
|
|
479
704
|
.optional()
|
|
480
705
|
.describe("For ai-instructions, the name or target to fetch; for conventions, the convention's name. Omit to list."),
|
|
481
|
-
},
|
|
706
|
+
}),
|
|
482
707
|
annotations: READ,
|
|
483
708
|
}, async ({ topic, name }) => {
|
|
484
709
|
const r = await reg();
|
|
710
|
+
if (topic === "bundu-floor")
|
|
711
|
+
return jsonContent({ floors: describeFloors(r.getBrand().data) });
|
|
485
712
|
if (topic === "ubuntu-pillars")
|
|
486
713
|
return respond(r.getUbuntuPillars(), "pillars");
|
|
487
714
|
if (topic === "ubuntu-principles")
|
|
@@ -493,14 +720,18 @@ export function registerMziziTools(server, opts) {
|
|
|
493
720
|
// 7 ─ get_skills — served from the bundle this Worker ships with
|
|
494
721
|
server.registerTool(note("get_skills"), {
|
|
495
722
|
title: "Get agent skills",
|
|
496
|
-
description: "List the published Mzizi agent skills, or fetch one in full. Skills are what an agent loads first. Each skill has a category: `dev` (how every agent works in every repo: digital-hygiene and progress-report — load these before any dev work), `design`, `language` or `build`. Pass `category` to get every skill in it in full (e.g. category=dev), or `name` for one skill. Served from the skill files bundled into this server at build time, so they are exactly the text in mzizi-dev/agent-tools at the deployed commit.",
|
|
497
|
-
inputSchema: {
|
|
498
|
-
name: z.string().optional().describe("Skill name. Omit to list all."),
|
|
723
|
+
description: "List the published Mzizi agent skills, or fetch one in full. Skills are what an agent loads first. Each skill has a category: `dev` (how every agent works in every repo: digital-hygiene and progress-report — load these before any dev work), `design`, `language` or `build`. Pass `category` to get every skill in it in full (e.g. category=dev), or `name` for one skill. A skill's answer lists the files it ships beside its SKILL.md (scripts such as hygiene.sh, token stylesheets); pass `name` and `file` to get one file's content. Each skill is also an MCP resource (mzizi://skills/<name>, files at mzizi://skills/<name>/<path>) and a prompt (skill-<name>). Served from the skill files bundled into this server at build time, so they are exactly the text in mzizi-dev/agent-tools at the deployed commit.",
|
|
724
|
+
inputSchema: z.object({
|
|
725
|
+
name: z.string().max(200).optional().describe("Skill name. Omit to list all."),
|
|
726
|
+
file: z
|
|
727
|
+
.string().max(500)
|
|
728
|
+
.optional()
|
|
729
|
+
.describe("With `name`: a file the skill ships, by the path its `files` list gives (e.g. hygiene.sh). Returns that file's content."),
|
|
499
730
|
category: z
|
|
500
731
|
.enum(SKILL_CATEGORIES)
|
|
501
732
|
.optional()
|
|
502
733
|
.describe("Return every skill in this category in full: dev (digital-hygiene, progress-report — for every agent in every repo), design, language or build."),
|
|
503
|
-
},
|
|
734
|
+
}),
|
|
504
735
|
annotations: READ,
|
|
505
736
|
},
|
|
506
737
|
// No network call, and that is the point. This used to read
|
|
@@ -509,7 +740,9 @@ export function registerMziziTools(server, opts) {
|
|
|
509
740
|
// instructing agents to write into a database column that had been cleared.
|
|
510
741
|
// The Worker now answers from the bundle it was built with: one hop, and a
|
|
511
742
|
// merge is the only way the answer changes.
|
|
512
|
-
async ({ name, category }) => {
|
|
743
|
+
async ({ name, category, file }) => {
|
|
744
|
+
if (file && !name)
|
|
745
|
+
return toolError("`file` needs `name`: the skill whose file it is.");
|
|
513
746
|
if (name) {
|
|
514
747
|
const skill = BUNDLED_SKILLS.find((s) => s.name === name);
|
|
515
748
|
if (!skill) {
|
|
@@ -518,7 +751,14 @@ export function registerMziziTools(server, opts) {
|
|
|
518
751
|
if (category && skill.category !== category) {
|
|
519
752
|
return toolError(`Skill "${name}" is in category "${skill.category}", not "${category}".`);
|
|
520
753
|
}
|
|
521
|
-
|
|
754
|
+
if (file) {
|
|
755
|
+
const f = skill.files.find((x) => x.path === file);
|
|
756
|
+
if (!f) {
|
|
757
|
+
return toolError(`Skill "${name}" ships no file "${file}". Its files: ${skill.files.map((x) => x.path).join(", ") || "none"}.`);
|
|
758
|
+
}
|
|
759
|
+
return jsonContent({ version: SKILLS_VERSION, skill: name, path: f.path, mimeType: f.mimeType, bytes: f.bytes, uri: skillFileUri(name, f.path), encoding: f.encoding, content: f.content }, "skills");
|
|
760
|
+
}
|
|
761
|
+
return jsonContent({ version: SKILLS_VERSION, ...skillView(skill) }, "skills");
|
|
522
762
|
}
|
|
523
763
|
if (category) {
|
|
524
764
|
// A category is a working set an agent loads together (category=dev is
|
|
@@ -526,7 +766,7 @@ export function registerMziziTools(server, opts) {
|
|
|
526
766
|
return jsonContent({
|
|
527
767
|
version: SKILLS_VERSION,
|
|
528
768
|
category,
|
|
529
|
-
skills: BUNDLED_SKILLS.filter((s) => s.category === category),
|
|
769
|
+
skills: BUNDLED_SKILLS.filter((s) => s.category === category).map(skillView),
|
|
530
770
|
}, "skills");
|
|
531
771
|
}
|
|
532
772
|
// The list omits bodies — seven full skills is ~170 kB, and a client
|
|
@@ -534,66 +774,125 @@ export function registerMziziTools(server, opts) {
|
|
|
534
774
|
return jsonContent({
|
|
535
775
|
version: SKILLS_VERSION,
|
|
536
776
|
categories: SKILL_CATEGORIES,
|
|
537
|
-
skills: BUNDLED_SKILLS.map(({ name: n, category: c, description, source }) => ({
|
|
777
|
+
skills: BUNDLED_SKILLS.map(({ name: n, category: c, description, source, files }) => ({
|
|
538
778
|
name: n,
|
|
539
779
|
category: c,
|
|
540
780
|
description,
|
|
541
781
|
source,
|
|
782
|
+
uri: skillUri(n),
|
|
783
|
+
files: files.map((f) => f.path),
|
|
542
784
|
})),
|
|
543
785
|
}, "skills");
|
|
544
786
|
});
|
|
545
787
|
// 8 ─ check_accessibility
|
|
546
788
|
server.registerTool(note("check_accessibility"), {
|
|
547
789
|
title: "Check accessibility",
|
|
548
|
-
description: "
|
|
549
|
-
inputSchema: {
|
|
550
|
-
foreground: z.string().describe("Foreground colour as #RRGGBB."),
|
|
551
|
-
background: z.string().describe("Background colour as #RRGGBB."),
|
|
552
|
-
|
|
790
|
+
description: "Check a design against the Mzizi floors. Colours: the WCAG 2.x contrast ratio of a foreground on a background (#RRGGBB), with the AA-large, AA and AAA verdicts and whether it meets the Mzizi floor of 7:1. Spacing: pass any of `insetPx` (text to container edge), `controlGapPx`, `labelGapPx`, `itemGapPx` (gaps) and `leading` (body line height) and each is compared with the spacing floor the pinned registry states (nothing may go under it): " + SPACING_PHRASE + ". Give colours, spacing values, or both. Computed locally, no network. It does not compute APCA Lc or simulate colour-vision deficiency; read the Mzizi accessibility doctrine (mzizi_get_tokens family=accessibility) for those rules. To check a project's source rather than values, run `mzizi check` (@nyuchi/mzizi-cli).",
|
|
791
|
+
inputSchema: z.object({
|
|
792
|
+
foreground: z.string().max(16).optional().describe("Foreground colour as #RRGGBB, e.g. #1B1A17. Give with background."),
|
|
793
|
+
background: z.string().max(16).optional().describe("Background colour as #RRGGBB, e.g. #FFFFFF. Give with foreground."),
|
|
794
|
+
insetPx: z.number().min(0).max(1000).optional().describe("Padding between text and its container edge, in px."),
|
|
795
|
+
controlGapPx: z.number().min(0).max(1000).optional().describe("Gap between adjacent interactive controls, visible edge to visible edge, in px."),
|
|
796
|
+
labelGapPx: z.number().min(0).max(1000).optional().describe("Gap between a label and its field, in px."),
|
|
797
|
+
itemGapPx: z.number().min(0).max(1000).optional().describe("Gap between sibling items in a list, grid or toolbar, in px."),
|
|
798
|
+
leading: z.number().min(0).max(10).optional().describe("Body line height as a unitless ratio, e.g. 1.5."),
|
|
799
|
+
}),
|
|
800
|
+
outputSchema: z.object({
|
|
801
|
+
foreground: z.string().optional(),
|
|
802
|
+
background: z.string().optional(),
|
|
803
|
+
contrastRatio: z.number().optional(),
|
|
804
|
+
passes: z.object({ aaLarge: z.boolean(), aa: z.boolean(), aaa: z.boolean() }).optional(),
|
|
805
|
+
meetsMziziFloor: z.boolean().optional(),
|
|
806
|
+
spacingUnavailable: z.string().optional(),
|
|
807
|
+
spacing: z
|
|
808
|
+
.object({
|
|
809
|
+
floor: z.object({
|
|
810
|
+
insetPx: z.number(),
|
|
811
|
+
controlGapPx: z.number(),
|
|
812
|
+
labelGapPx: z.number(),
|
|
813
|
+
itemGapPx: z.number(),
|
|
814
|
+
leadingBody: z.number(),
|
|
815
|
+
}),
|
|
816
|
+
checks: z.array(z.object({
|
|
817
|
+
name: z.string(),
|
|
818
|
+
value: z.number(),
|
|
819
|
+
minimum: z.number(),
|
|
820
|
+
unit: z.enum(["px", "ratio"]),
|
|
821
|
+
passes: z.boolean(),
|
|
822
|
+
})),
|
|
823
|
+
meetsFloor: z.boolean(),
|
|
824
|
+
})
|
|
825
|
+
.optional(),
|
|
826
|
+
}),
|
|
553
827
|
annotations: READ,
|
|
554
|
-
}, async ({ foreground, background }) => {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
return
|
|
828
|
+
}, async ({ foreground, background, insetPx, controlGapPx, labelGapPx, itemGapPx, leading }) => {
|
|
829
|
+
const hasSpacing = [insetPx, controlGapPx, labelGapPx, itemGapPx, leading].some((v) => v !== undefined);
|
|
830
|
+
const hasColours = foreground !== undefined || background !== undefined;
|
|
831
|
+
if (!hasSpacing && !hasColours) {
|
|
832
|
+
return toolError("Give a foreground and background colour (#RRGGBB), spacing values (insetPx, controlGapPx, labelGapPx, itemGapPx, leading), or both.");
|
|
833
|
+
}
|
|
834
|
+
if (hasColours && (foreground === undefined || background === undefined)) {
|
|
835
|
+
return toolError("Give both foreground and background, e.g. #1B1A17 on #FFFFFF.");
|
|
836
|
+
}
|
|
837
|
+
const out = {};
|
|
838
|
+
if (foreground !== undefined && background !== undefined) {
|
|
839
|
+
const parse = (hex) => {
|
|
840
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
841
|
+
if (!m?.[1])
|
|
842
|
+
return null;
|
|
843
|
+
const n = Number.parseInt(m[1], 16);
|
|
844
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
845
|
+
};
|
|
846
|
+
const fg = parse(foreground);
|
|
847
|
+
const bg = parse(background);
|
|
848
|
+
if (!fg || !bg)
|
|
849
|
+
return toolError("Both colours must be 6-digit hex, e.g. #1B1A17.");
|
|
850
|
+
// WCAG relative luminance.
|
|
851
|
+
const lum = ([r, g, b]) => {
|
|
852
|
+
const [cr, cg, cb] = [r, g, b].map((v) => {
|
|
853
|
+
const s = v / 255;
|
|
854
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
855
|
+
});
|
|
856
|
+
return 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
|
|
857
|
+
};
|
|
858
|
+
const l1 = lum(fg);
|
|
859
|
+
const l2 = lum(bg);
|
|
860
|
+
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
861
|
+
const rounded = Math.round(ratio * 100) / 100;
|
|
862
|
+
Object.assign(out, {
|
|
863
|
+
foreground,
|
|
864
|
+
background,
|
|
865
|
+
contrastRatio: rounded,
|
|
866
|
+
passes: {
|
|
867
|
+
aaLarge: rounded >= 3,
|
|
868
|
+
aa: rounded >= 4.5,
|
|
869
|
+
aaa: rounded >= 7,
|
|
870
|
+
},
|
|
871
|
+
// Mzizi's floor is AAA (7:1), stricter than WCAG AA -- say so rather than
|
|
872
|
+
// letting a 4.5 look like a pass here.
|
|
873
|
+
meetsMziziFloor: rounded >= 7,
|
|
571
874
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
// Mzizi's floor is AAA (7:1), stricter than WCAG AA -- say so rather than
|
|
588
|
-
// letting a 4.5 look like a pass here.
|
|
589
|
-
meetsMziziFloor: rounded >= 7,
|
|
590
|
-
});
|
|
875
|
+
}
|
|
876
|
+
if (hasSpacing) {
|
|
877
|
+
const floor = spacingFloorFrom((await reg()).getBrand().data);
|
|
878
|
+
if (floor) {
|
|
879
|
+
out.spacing = checkSpacing(floor, { insetPx, controlGapPx, labelGapPx, itemGapPx, leading });
|
|
880
|
+
}
|
|
881
|
+
else if (!hasColours) {
|
|
882
|
+
return toolError(SPACING_FLOOR_MISSING);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// The colour result below is computed locally and valid: keep it, and say why spacing is absent.
|
|
886
|
+
out.spacingUnavailable = SPACING_FLOOR_MISSING;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return jsonContent(out);
|
|
591
890
|
});
|
|
592
891
|
// 9 ─ report_issue — the write sink, backed by the per-user issue desk
|
|
593
892
|
server.registerTool(note("report_issue"), {
|
|
594
893
|
title: "Report an issue",
|
|
595
894
|
description: "Report a problem with a component or the registry. It goes to your own issue desk — a Durable Object holding your reports — which reproduces it via fundi, drafts an issue body including what the reproduction actually returned, and logs it to fundi. fundi files the GitHub issue with its proposed fix in it and tracks that issue afterwards, so action 'get' shows where yours got to: filed, picked up, closed. Use 'list' to see your earlier reports and 'retry' to re-attempt one that drafted but could not be logged. Your history is included in each new issue, because a repeated complaint from one reporter is often one bug. Needs sign-in: the desk belongs to your Mzizi console user.",
|
|
596
|
-
inputSchema: {
|
|
895
|
+
inputSchema: z.object({
|
|
597
896
|
action: z
|
|
598
897
|
.enum(["file", "list", "get", "retry"])
|
|
599
898
|
.default("file")
|
|
@@ -603,24 +902,24 @@ export function registerMziziTools(server, opts) {
|
|
|
603
902
|
.optional()
|
|
604
903
|
.describe("Report kind. Required for action 'file'."),
|
|
605
904
|
component: z
|
|
606
|
-
.string()
|
|
905
|
+
.string().max(200)
|
|
607
906
|
.optional()
|
|
608
907
|
.describe("Component the report concerns."),
|
|
609
908
|
message: z
|
|
610
|
-
.string()
|
|
909
|
+
.string().max(20000)
|
|
611
910
|
.optional()
|
|
612
911
|
.describe("What happened. Required for action 'file'."),
|
|
613
912
|
severity: z.enum(["low", "medium", "high", "critical"]).optional(),
|
|
614
913
|
skill: z
|
|
615
|
-
.string()
|
|
914
|
+
.string().max(100)
|
|
616
915
|
.optional()
|
|
617
916
|
.describe("fundi skill to run for the reproduction. fundi never infers a skill from text, so omit this to let the report kind choose one."),
|
|
618
917
|
reportId: z
|
|
619
|
-
.string()
|
|
918
|
+
.string().max(200)
|
|
620
919
|
.optional()
|
|
621
920
|
.describe("Report id. Required for actions 'get' and 'retry'."),
|
|
622
921
|
limit: z.number().int().min(1).max(100).optional(),
|
|
623
|
-
},
|
|
922
|
+
}),
|
|
624
923
|
annotations: WRITE,
|
|
625
924
|
}, async (args) => {
|
|
626
925
|
const desk = opts.desk;
|
|
@@ -673,21 +972,21 @@ export function registerMziziTools(server, opts) {
|
|
|
673
972
|
server.registerTool(note("fundi"), {
|
|
674
973
|
title: "fundi",
|
|
675
974
|
description: "Delegate a task to the fundi agent, or check on one. Submit-and-poll: `submit` returns a task id immediately and never waits for the run. Needs sign-in: fundi runs as your Mzizi console user.",
|
|
676
|
-
inputSchema: {
|
|
975
|
+
inputSchema: z.object({
|
|
677
976
|
action: z.enum(["submit", "task_status", "cancel"]),
|
|
678
977
|
skill: z
|
|
679
978
|
.enum(["security-scan", "chaos-run", "a11y-audit", "heal-issue"])
|
|
680
979
|
.optional()
|
|
681
980
|
.describe("Required for submit."),
|
|
682
981
|
instruction: z
|
|
683
|
-
.string()
|
|
982
|
+
.string().max(20000)
|
|
684
983
|
.optional()
|
|
685
984
|
.describe("What fundi should do. Required for submit."),
|
|
686
985
|
taskId: z
|
|
687
|
-
.string()
|
|
986
|
+
.string().max(200)
|
|
688
987
|
.optional()
|
|
689
988
|
.describe("Required for task_status and cancel."),
|
|
690
|
-
},
|
|
989
|
+
}),
|
|
691
990
|
annotations: { ...WRITE, destructiveHint: true },
|
|
692
991
|
}, async ({ action, skill, instruction, taskId }) => {
|
|
693
992
|
if (!f)
|