@nyuchi/mzizi-mcp 0.6.0 → 0.7.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/dist/api-client.d.ts +86 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +179 -0
- package/dist/api-client.js.map +1 -0
- package/dist/http.js +1 -1
- package/dist/http.js.map +1 -1
- package/dist/server.d.ts +24 -26
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +52 -709
- package/dist/server.js.map +1 -1
- package/dist/stdio.js +1 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tools.d.ts +30 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +307 -0
- package/dist/tools.js.map +1 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,457 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mzizi MCP
|
|
2
|
+
* The Mzizi MCP server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `mcp_tool_registry` table (the same registry that powers the design system's
|
|
6
|
-
* MCP), and each tool is dispatched dynamically to its backing implementation:
|
|
4
|
+
* Backed by `mzizi.dev/api/v1` over HTTP. There is no Supabase client here.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* WHAT CHANGED AND WHY. This server used to build its tool catalog at startup from the
|
|
7
|
+
* `mcp_tool_registry` table and dispatch each call to a Supabase RPC, edge function or
|
|
8
|
+
* table read — 62 enabled tools, defined as data. Two things were wrong with that:
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* 1. Components are not rows. They are real files in `nyuchi/mzizi` that the Next.js
|
|
11
|
+
* app compiles and typechecks, so an error in one fails the build. The app serves
|
|
12
|
+
* them over `/api/v1/*`, which makes that API the source of truth and this server
|
|
13
|
+
* one of its clients. Reading the database put a second, uncompiled representation
|
|
14
|
+
* in the path — one that could disagree with the code while still looking correct.
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* and EXECUTE on the document/registry RPCs.
|
|
22
|
-
*/
|
|
23
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
-
import { FUNDI_A2A_TOOLS, dispatchFundiA2A, isFundiA2AToolName, } from "./fundi-a2a.js";
|
|
26
|
-
import { jsonContent, toolError } from "./tool-result.js";
|
|
27
|
-
export const MZIZI_MCP_VERSION = "0.6.0";
|
|
28
|
-
/**
|
|
29
|
-
* Every tool this server exposes is prefixed with `mzizi_` so the surface is
|
|
30
|
-
* unambiguous when mounted alongside other MCP servers (see the mcp-builder
|
|
31
|
-
* "service prefix" guideline). The prefix is applied at the boundary only:
|
|
32
|
-
* registry rows, core-tool dispatch, and the backing SQL/edge/table names all
|
|
33
|
-
* stay unprefixed, and the CallTool handler strips the prefix before dispatch.
|
|
34
|
-
*/
|
|
35
|
-
const TOOL_PREFIX = "mzizi_";
|
|
36
|
-
/** Human-readable title from a (possibly prefixed) tool name: `get_component` → "Get component". */
|
|
37
|
-
function titleFor(name) {
|
|
38
|
-
const words = name
|
|
39
|
-
.replace(new RegExp(`^${TOOL_PREFIX}`), "")
|
|
40
|
-
.replace(/_/g, " ")
|
|
41
|
-
.trim();
|
|
42
|
-
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
43
|
-
}
|
|
44
|
-
const READ_ONLY_HINTS = {
|
|
45
|
-
readOnlyHint: true,
|
|
46
|
-
destructiveHint: false,
|
|
47
|
-
idempotentHint: true,
|
|
48
|
-
openWorldHint: false,
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Defaults for a mutating tool: not read-only, but append-only (never deletes
|
|
52
|
-
* or overwrites → `destructiveHint` false) and not idempotent (each call makes
|
|
53
|
-
* a new row). Also the fallback for `action`-kind tools, which run/compute
|
|
54
|
-
* something and are likewise neither read-only nor idempotent.
|
|
55
|
-
*/
|
|
56
|
-
const WRITE_HINTS = {
|
|
57
|
-
readOnlyHint: false,
|
|
58
|
-
destructiveHint: false,
|
|
59
|
-
idempotentHint: false,
|
|
60
|
-
openWorldHint: false,
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* The only write-kind tools allowed onto the anonymous public surface: the
|
|
64
|
-
* feedback sinks that either run SECURITY DEFINER (bypassing RLS safely) or
|
|
65
|
-
* self-gate to an allow-list. Every other write stays off this surface and is
|
|
66
|
-
* reached through an authenticated path (fundi worker / first-party caller).
|
|
67
|
-
* • submit_component_feedback — report a failing/misconfigured component (N9)
|
|
68
|
-
* • record_observability_event — structural error/perf beacon fundi watches
|
|
69
|
-
* • record_chaos_event — resilience-testing feedback (self-gates in prod)
|
|
70
|
-
*/
|
|
71
|
-
const PUBLIC_WRITE_TOOLS = new Set([
|
|
72
|
-
"submit_component_feedback",
|
|
73
|
-
"record_observability_event",
|
|
74
|
-
"record_chaos_event",
|
|
75
|
-
]);
|
|
76
|
-
/**
|
|
77
|
-
* Annotations for a CORE tool defined in this file (list_collections,
|
|
78
|
-
* get_database_status, fundi_status, the registry-unavailable diagnostic).
|
|
79
|
-
* These have no registry row, so their hints live here. Defaults to read-only;
|
|
80
|
-
* pass overrides for the exceptions.
|
|
81
|
-
*/
|
|
82
|
-
function annotationsFor(name, overrides = {}) {
|
|
83
|
-
return { title: titleFor(name), ...READ_ONLY_HINTS, ...overrides };
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Annotations for a registry-backed tool, read from the `mcp_tool_registry`
|
|
87
|
-
* annotation columns — the single source of truth. Each column that is null
|
|
88
|
-
* falls back to the per-`tool_kind` default (`read` → READ_ONLY_HINTS, anything
|
|
89
|
-
* else → WRITE_HINTS), and `title` falls back to the derived `titleFor`. This
|
|
90
|
-
* keeps the emitted annotations honest for every kind, including `action` tools
|
|
91
|
-
* (which compute/run and are correctly NOT read-only), without hardcoding any
|
|
92
|
-
* per-tool behaviour in the server.
|
|
93
|
-
*/
|
|
94
|
-
function annotationsFromRow(row) {
|
|
95
|
-
const fallback = row.tool_kind === "read" ? READ_ONLY_HINTS : WRITE_HINTS;
|
|
96
|
-
const pick = (col, def) => col ?? def;
|
|
97
|
-
return {
|
|
98
|
-
title: row.title ?? titleFor(row.tool_name),
|
|
99
|
-
readOnlyHint: pick(row.read_only_hint, fallback.readOnlyHint),
|
|
100
|
-
destructiveHint: pick(row.destructive_hint, fallback.destructiveHint),
|
|
101
|
-
idempotentHint: pick(row.idempotent_hint, fallback.idempotentHint),
|
|
102
|
-
openWorldHint: pick(row.open_world_hint, fallback.openWorldHint),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Namespaced `_meta` for a tool. The registry groups tools by `category`
|
|
107
|
-
* (component, architecture, brand, skills, documents, release, a11y,
|
|
108
|
-
* observability, fundi, …); surfacing it here lets clients organise the ~60
|
|
109
|
-
* tools by function. The `mzizi.dev/` prefix keeps the key out of the reserved
|
|
110
|
-
* `modelcontextprotocol.io/` namespace.
|
|
111
|
-
*/
|
|
112
|
-
function metaFor(category) {
|
|
113
|
-
return { "mzizi.dev/category": category ?? "uncategorised" };
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* The registry stores each tool's return shape as an already-enveloped JSON
|
|
117
|
-
* Schema object: `{ type:"object", properties:{ <key>: … }, required:[<key>] }`
|
|
118
|
-
* where `<key>` is `rows` for the array-returning `RETURNS TABLE` functions and
|
|
119
|
-
* `result` for scalar/jsonb returns. MCP requires `outputSchema` (and the
|
|
120
|
-
* matching `structuredContent`) to be an object, so the RPC's raw array/scalar
|
|
121
|
-
* result is wrapped under that single key at dispatch time. Returns the schema
|
|
122
|
-
* to emit plus the wrap key, or null when no usable shape is stored.
|
|
123
|
-
*/
|
|
124
|
-
function emittedOutput(raw) {
|
|
125
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
126
|
-
return null;
|
|
127
|
-
if (raw["type"] !== "object")
|
|
128
|
-
return null;
|
|
129
|
-
const props = raw["properties"];
|
|
130
|
-
if (!props || typeof props !== "object")
|
|
131
|
-
return null;
|
|
132
|
-
const keys = Object.keys(props);
|
|
133
|
-
if (keys.length === 0)
|
|
134
|
-
return null;
|
|
135
|
-
const required = Array.isArray(raw["required"])
|
|
136
|
-
? raw["required"]
|
|
137
|
-
: [];
|
|
138
|
-
const wrapKey = required[0] ?? keys[0];
|
|
139
|
-
if (!wrapKey)
|
|
140
|
-
return null;
|
|
141
|
-
return { outputSchema: raw, wrapKey };
|
|
142
|
-
}
|
|
143
|
-
const str = (description) => ({
|
|
144
|
-
type: ["string", "null"],
|
|
145
|
-
description,
|
|
146
|
-
});
|
|
147
|
-
const reqStr = (description) => ({ type: "string", description });
|
|
148
|
-
const int = (description) => ({
|
|
149
|
-
type: ["integer", "null"],
|
|
150
|
-
description,
|
|
151
|
-
});
|
|
152
|
-
const bool = (description) => ({
|
|
153
|
-
type: ["boolean", "null"],
|
|
154
|
-
description,
|
|
155
|
-
});
|
|
156
|
-
const num = (description) => ({
|
|
157
|
-
type: ["number", "null"],
|
|
158
|
-
description,
|
|
159
|
-
});
|
|
160
|
-
/**
|
|
161
|
-
* Curated input schemas for the parameterized tools whose registry
|
|
162
|
-
* `input_schema` is not (yet) populated. Property names are the exact SQL
|
|
163
|
-
* function parameter names so `rpc()` receives them unchanged. No-arg tools are
|
|
164
|
-
* omitted — they fall back to an empty-object schema.
|
|
16
|
+
* 2. A tool surface defined in a table changes whenever someone edits a row: no diff,
|
|
17
|
+
* no review, no test. The 10 tools live in `tools.ts` as code, so changing them is
|
|
18
|
+
* a pull request.
|
|
165
19
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* and has to be chased forever. An unknown node returning an empty set is the
|
|
170
|
-
* correct answer; a validation error is not. Use NODE_ARG_DESCRIPTION so this
|
|
171
|
-
* cannot drift back one property at a time.
|
|
172
|
-
*/
|
|
173
|
-
const NODE_ARG_DESCRIPTION = "Ecosystem node number. Nodes are labels, not a sequence, and are never " +
|
|
174
|
-
"capped — read the documentation-architecture-nodes collection for the live set.";
|
|
175
|
-
const INPUT_SCHEMAS = {
|
|
176
|
-
// component
|
|
177
|
-
get_component: {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: { p_name: reqStr("Component name, e.g. 'button'") },
|
|
180
|
-
required: ["p_name"],
|
|
181
|
-
},
|
|
182
|
-
get_component_history: {
|
|
183
|
-
type: "object",
|
|
184
|
-
properties: {
|
|
185
|
-
p_component_name: reqStr("Component name"),
|
|
186
|
-
p_limit: int("Max rows (default 20)"),
|
|
187
|
-
},
|
|
188
|
-
required: ["p_component_name"],
|
|
189
|
-
},
|
|
190
|
-
get_component_links: {
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: { p_name: reqStr("Component name") },
|
|
193
|
-
required: ["p_name"],
|
|
194
|
-
},
|
|
195
|
-
list_components: {
|
|
196
|
-
type: "object",
|
|
197
|
-
properties: {
|
|
198
|
-
p_node: int(NODE_ARG_DESCRIPTION),
|
|
199
|
-
p_status: str("Status filter"),
|
|
200
|
-
p_category: str("Category filter"),
|
|
201
|
-
p_limit: int("Max rows (default 500)"),
|
|
202
|
-
p_offset: int("Offset"),
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
search_components: {
|
|
206
|
-
type: "object",
|
|
207
|
-
properties: {
|
|
208
|
-
p_query: reqStr("Search query"),
|
|
209
|
-
p_limit: int("Max rows (default 50)"),
|
|
210
|
-
},
|
|
211
|
-
required: ["p_query"],
|
|
212
|
-
},
|
|
213
|
-
get_node_categories: {
|
|
214
|
-
type: "object",
|
|
215
|
-
properties: { p_node: int(`${NODE_ARG_DESCRIPTION} Omit for all nodes.`) },
|
|
216
|
-
},
|
|
217
|
-
get_node_detail: {
|
|
218
|
-
type: "object",
|
|
219
|
-
properties: {
|
|
220
|
-
p_node: int(`${NODE_ARG_DESCRIPTION} Omit for a summary of every node.`),
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
// documents
|
|
224
|
-
get_node_documents: {
|
|
225
|
-
type: "object",
|
|
226
|
-
properties: {
|
|
227
|
-
p_node: int(NODE_ARG_DESCRIPTION),
|
|
228
|
-
p_collection: str("Collection key, e.g. 'agentic-components'"),
|
|
229
|
-
p_name: str("Exact document name"),
|
|
230
|
-
p_owner: str("Owner: bundu | mzizi | framework | nyuchi"),
|
|
231
|
-
p_limit: int("Max rows (default 1000)"),
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
// skills
|
|
235
|
-
get_skill: {
|
|
236
|
-
type: "object",
|
|
237
|
-
properties: { p_name: reqStr("Skill name") },
|
|
238
|
-
required: ["p_name"],
|
|
239
|
-
},
|
|
240
|
-
// ai
|
|
241
|
-
get_ai_instructions: {
|
|
242
|
-
type: "object",
|
|
243
|
-
properties: { p_name: reqStr("Target/instruction name, e.g. 'claude'") },
|
|
244
|
-
required: ["p_name"],
|
|
245
|
-
},
|
|
246
|
-
list_ai_instructions: {
|
|
247
|
-
type: "object",
|
|
248
|
-
properties: { p_target: str("Filter by target") },
|
|
249
|
-
},
|
|
250
|
-
// brand
|
|
251
|
-
get_brand_tokens: {
|
|
252
|
-
type: "object",
|
|
253
|
-
properties: { p_category: reqStr("Token category") },
|
|
254
|
-
required: ["p_category"],
|
|
255
|
-
},
|
|
256
|
-
list_ecosystem_brands: {
|
|
257
|
-
type: "object",
|
|
258
|
-
properties: { p_adopter_type: str("Adopter type filter") },
|
|
259
|
-
},
|
|
260
|
-
// governance
|
|
261
|
-
get_bundu_convention: {
|
|
262
|
-
type: "object",
|
|
263
|
-
properties: { p_slug: reqStr("Convention slug") },
|
|
264
|
-
required: ["p_slug"],
|
|
265
|
-
},
|
|
266
|
-
// release
|
|
267
|
-
get_changelog_entry: {
|
|
268
|
-
type: "object",
|
|
269
|
-
properties: { p_version: reqStr("Version, e.g. '7.0.0'") },
|
|
270
|
-
required: ["p_version"],
|
|
271
|
-
},
|
|
272
|
-
get_component_status_history: {
|
|
273
|
-
type: "object",
|
|
274
|
-
properties: { p_name: reqStr("Component name") },
|
|
275
|
-
required: ["p_name"],
|
|
276
|
-
},
|
|
277
|
-
get_release_with_diff: {
|
|
278
|
-
type: "object",
|
|
279
|
-
properties: { p_version: reqStr("Version") },
|
|
280
|
-
required: ["p_version"],
|
|
281
|
-
},
|
|
282
|
-
compute_release_diff: {
|
|
283
|
-
type: "object",
|
|
284
|
-
properties: {
|
|
285
|
-
p_since: reqStr("ISO timestamp (inclusive)"),
|
|
286
|
-
p_until: str("ISO timestamp (default now)"),
|
|
287
|
-
},
|
|
288
|
-
required: ["p_since"],
|
|
289
|
-
},
|
|
290
|
-
list_changelog: {
|
|
291
|
-
type: "object",
|
|
292
|
-
properties: {
|
|
293
|
-
p_limit: int("Max rows (default 50)"),
|
|
294
|
-
p_offset: int("Offset"),
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
list_tool_versions: {
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: {
|
|
300
|
-
p_tool_name: reqStr("Tool name"),
|
|
301
|
-
p_limit: int("Max rows (default 20)"),
|
|
302
|
-
},
|
|
303
|
-
required: ["p_tool_name"],
|
|
304
|
-
},
|
|
305
|
-
// fundi (reads)
|
|
306
|
-
get_healing_log: {
|
|
307
|
-
type: "object",
|
|
308
|
-
properties: {
|
|
309
|
-
p_issue_id: int("Filter by issue id"),
|
|
310
|
-
p_limit: int("Max rows (default 100)"),
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
list_recent_fundi_issues: {
|
|
314
|
-
type: "object",
|
|
315
|
-
properties: {
|
|
316
|
-
p_status: str("Status filter"),
|
|
317
|
-
p_severity: str("Severity filter"),
|
|
318
|
-
p_limit: int("Max rows (default 50)"),
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
// resolver
|
|
322
|
-
resolve_primitive: {
|
|
323
|
-
type: "object",
|
|
324
|
-
properties: {
|
|
325
|
-
p_primitive_name: reqStr("Primitive name"),
|
|
326
|
-
p_descriptor_slug: reqStr("Framework descriptor slug, e.g. 'react'"),
|
|
327
|
-
},
|
|
328
|
-
required: ["p_primitive_name", "p_descriptor_slug"],
|
|
329
|
-
},
|
|
330
|
-
list_primitive_sources: {
|
|
331
|
-
type: "object",
|
|
332
|
-
properties: { p_primitive_name: reqStr("Primitive name") },
|
|
333
|
-
required: ["p_primitive_name"],
|
|
334
|
-
},
|
|
335
|
-
list_framework_descriptors: {
|
|
336
|
-
type: "object",
|
|
337
|
-
properties: { p_active_only: bool("Only active descriptors") },
|
|
338
|
-
},
|
|
339
|
-
// observability
|
|
340
|
-
is_domain_allowed: {
|
|
341
|
-
type: "object",
|
|
342
|
-
properties: {
|
|
343
|
-
p_domain: reqStr("Domain"),
|
|
344
|
-
p_feature: str("Feature (default 'portal_links')"),
|
|
345
|
-
},
|
|
346
|
-
required: ["p_domain"],
|
|
347
|
-
},
|
|
348
|
-
list_observability_domains: {
|
|
349
|
-
type: "object",
|
|
350
|
-
properties: { p_feature: str("Feature filter") },
|
|
351
|
-
},
|
|
352
|
-
list_observability_events: {
|
|
353
|
-
type: "object",
|
|
354
|
-
properties: {
|
|
355
|
-
p_domain: str("Domain filter"),
|
|
356
|
-
p_component_name: str("Component filter"),
|
|
357
|
-
p_event_type: str("Event-type filter"),
|
|
358
|
-
p_limit: int("Max rows (default 100)"),
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
// chaos (read)
|
|
362
|
-
list_chaos_events: {
|
|
363
|
-
type: "object",
|
|
364
|
-
properties: {
|
|
365
|
-
p_domain: str("Domain filter"),
|
|
366
|
-
p_environment: str("Environment filter"),
|
|
367
|
-
p_event_type: str("Event-type filter"),
|
|
368
|
-
p_limit: int("Max rows (default 100)"),
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
// a11y
|
|
372
|
-
calculate_contrast_ratio: {
|
|
373
|
-
type: "object",
|
|
374
|
-
properties: {
|
|
375
|
-
fg_hex: reqStr("Foreground hex, e.g. '#111111'"),
|
|
376
|
-
bg_hex: reqStr("Background hex, e.g. '#ffffff'"),
|
|
377
|
-
},
|
|
378
|
-
required: ["fg_hex", "bg_hex"],
|
|
379
|
-
},
|
|
380
|
-
relative_luminance: {
|
|
381
|
-
type: "object",
|
|
382
|
-
properties: { hex: reqStr("Hex colour") },
|
|
383
|
-
required: ["hex"],
|
|
384
|
-
},
|
|
385
|
-
simulate_color_blindness: {
|
|
386
|
-
type: "object",
|
|
387
|
-
properties: {
|
|
388
|
-
hex: reqStr("Hex colour"),
|
|
389
|
-
cb_type: reqStr("Type: protanopia | deuteranopia | tritanopia"),
|
|
390
|
-
},
|
|
391
|
-
required: ["hex", "cb_type"],
|
|
392
|
-
},
|
|
393
|
-
run_accessibility_audit: {
|
|
394
|
-
type: "object",
|
|
395
|
-
properties: {
|
|
396
|
-
p_file_fundi_issues: bool("File fundi issues for failures (default true)"),
|
|
397
|
-
p_contrast_floor: num("Minimum contrast ratio (default 3.0)"),
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
// meta
|
|
401
|
-
mcp_describe: {
|
|
402
|
-
type: "object",
|
|
403
|
-
properties: {
|
|
404
|
-
p_category: str("Filter by category"),
|
|
405
|
-
p_stability: str("Filter by stability"),
|
|
406
|
-
p_tool_kind: str("Filter by tool kind"),
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
const EMPTY_SCHEMA = { type: "object", properties: {} };
|
|
411
|
-
/**
|
|
412
|
-
* Fetch every `(collection, node, owner)` row from the document store, paging
|
|
413
|
-
* past PostgREST's max-rows cap (1000 by default). The store holds >3k rows;
|
|
414
|
-
* a single unpaged select silently truncates the aggregate — counts come back
|
|
415
|
-
* wrong and whole collections (shell, safety, agentic-components, …) vanish
|
|
416
|
-
* from the surface. Ordered by the `(collection, name)` primary key so pages
|
|
417
|
-
* are stable.
|
|
20
|
+
* The 10 are specified in `docs/mcp-tool-consolidation.md` (mzizi repo). Each carries its
|
|
21
|
+
* `replaces` list in `TOOL_COVERAGE`, so coverage of the old 62 is checkable rather than
|
|
22
|
+
* asserted.
|
|
418
23
|
*/
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.select("collection, node, owner")
|
|
426
|
-
.order("collection", { ascending: true })
|
|
427
|
-
.order("name", { ascending: true })
|
|
428
|
-
.range(from, from + page - 1);
|
|
429
|
-
if (error)
|
|
430
|
-
return { rows, error: error.message };
|
|
431
|
-
const batch = data ?? [];
|
|
432
|
-
rows.push(...batch);
|
|
433
|
-
if (batch.length < page)
|
|
434
|
-
return { rows, error: null };
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
function hasProperties(schema) {
|
|
438
|
-
const props = schema && schema.properties;
|
|
439
|
-
return (!!props &&
|
|
440
|
-
typeof props === "object" &&
|
|
441
|
-
Object.keys(props).length > 0);
|
|
442
|
-
}
|
|
443
|
-
function schemaFor(row) {
|
|
444
|
-
if (hasProperties(row.input_schema))
|
|
445
|
-
return row.input_schema;
|
|
446
|
-
return INPUT_SCHEMAS[row.tool_name] ?? EMPTY_SCHEMA;
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Build the Mzizi MCP server bound to a Supabase client with public read on
|
|
450
|
-
* `mcp_tool_registry` / `component_documents` and EXECUTE on the backing RPCs.
|
|
451
|
-
* The tool catalog is loaded from the registry; write-kind tools are excluded
|
|
452
|
-
* from this (anonymous, public) surface.
|
|
453
|
-
*/
|
|
454
|
-
export async function createMziziMcpServer(supabase, opts = {}) {
|
|
24
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
|
+
import { MziziApiClient } from "./api-client.js";
|
|
26
|
+
import { registerMziziTools, TOOL_COVERAGE, TOOL_PREFIX } from "./tools.js";
|
|
27
|
+
import { jsonContent } from "./tool-result.js";
|
|
28
|
+
export const MZIZI_MCP_VERSION = "0.7.0";
|
|
29
|
+
export async function createMziziMcpServer(opts = {}) {
|
|
455
30
|
const server = new McpServer({
|
|
456
31
|
name: "mzizi-mcp",
|
|
457
32
|
version: MZIZI_MCP_VERSION,
|
|
@@ -461,271 +36,39 @@ export async function createMziziMcpServer(supabase, opts = {}) {
|
|
|
461
36
|
tools: { listChanged: true },
|
|
462
37
|
resources: { listChanged: true },
|
|
463
38
|
},
|
|
464
|
-
instructions: "
|
|
39
|
+
instructions: "Access to the Mzizi design system (a nyuchi-brand project of the Bundu ecosystem) over its public API: components, design tokens, brand, the DNA-helix architecture, Ubuntu doctrine, agent skills and releases. Ten tools. Start with mzizi_search if you do not know the name of what you want; mzizi_get_component returns a component in full in one read; mzizi_get_architecture returns the helix and takes an uncapped node number.",
|
|
465
40
|
});
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
// Core document tools that are aggregates without an RPC, so they aren't
|
|
490
|
-
// registry-backed. Everything else is registry-driven.
|
|
491
|
-
const collectionsOutput = {
|
|
492
|
-
type: "object",
|
|
493
|
-
properties: {
|
|
494
|
-
rows: {
|
|
495
|
-
type: "array",
|
|
496
|
-
items: {
|
|
497
|
-
type: "object",
|
|
498
|
-
properties: {
|
|
499
|
-
collection: { type: "string" },
|
|
500
|
-
node: { type: ["integer", "null"] },
|
|
501
|
-
total: { type: "integer" },
|
|
502
|
-
byOwner: { type: "object" },
|
|
503
|
-
},
|
|
504
|
-
},
|
|
41
|
+
const api = new MziziApiClient({ baseUrl: opts.apiBaseUrl, fetchImpl: opts.fetchImpl });
|
|
42
|
+
const registered = registerMziziTools(server, { api, fundi: opts.fundi });
|
|
43
|
+
const describe = () => ({
|
|
44
|
+
version: MZIZI_MCP_VERSION,
|
|
45
|
+
source: "https://mzizi.dev/api/v1",
|
|
46
|
+
note: "Components are files in nyuchi/mzizi compiled by the Next.js app and served over /api/v1. This server reads that API and holds no database credential.",
|
|
47
|
+
tools: registered.map((name) => ({
|
|
48
|
+
name: `${TOOL_PREFIX}${name}`,
|
|
49
|
+
replaces: TOOL_COVERAGE[name] ?? [],
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
// Computed from what was actually registered, never a hand-maintained list — a
|
|
53
|
+
// hardcoded catalog is how the old registry drifted from the tools it described.
|
|
54
|
+
server.registerResource("mzizi-tools", "mzizi://tools", {
|
|
55
|
+
title: "Mzizi MCP tools",
|
|
56
|
+
description: "The tools this server exposes, and which legacy tools each replaces.",
|
|
57
|
+
mimeType: "application/json",
|
|
58
|
+
}, async (uri) => ({
|
|
59
|
+
contents: [
|
|
60
|
+
{
|
|
61
|
+
uri: uri.href,
|
|
62
|
+
mimeType: "application/json",
|
|
63
|
+
text: JSON.stringify(describe(), null, 2),
|
|
505
64
|
},
|
|
506
|
-
|
|
507
|
-
required: ["rows"],
|
|
508
|
-
};
|
|
509
|
-
const databaseStatusOutput = {
|
|
510
|
-
type: "object",
|
|
511
|
-
properties: {
|
|
512
|
-
provider: { type: "string" },
|
|
513
|
-
status: { type: "string" },
|
|
514
|
-
error: { type: ["string", "null"] },
|
|
515
|
-
documents: { type: "integer" },
|
|
516
|
-
registryTools: { type: "integer" },
|
|
517
|
-
version: { type: "string" },
|
|
518
|
-
readOnly: { type: "boolean" },
|
|
519
|
-
},
|
|
520
|
-
required: ["provider", "status", "documents", "registryTools", "version"],
|
|
521
|
-
};
|
|
522
|
-
const coreTools = [
|
|
523
|
-
{
|
|
524
|
-
name: `${TOOL_PREFIX}list_collections`,
|
|
525
|
-
description: "List every collection in the Mzizi document store with document counts and per-owner breakdown (components, primitives, brand, agentic-components, skills, …).",
|
|
526
|
-
inputSchema: EMPTY_SCHEMA,
|
|
527
|
-
outputSchema: collectionsOutput,
|
|
528
|
-
annotations: annotationsFor("list_collections"),
|
|
529
|
-
_meta: metaFor("documents"),
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
name: `${TOOL_PREFIX}get_database_status`,
|
|
533
|
-
description: "Provider health, document-store row count, and registered-tool count.",
|
|
534
|
-
inputSchema: EMPTY_SCHEMA,
|
|
535
|
-
outputSchema: databaseStatusOutput,
|
|
536
|
-
annotations: annotationsFor("get_database_status"),
|
|
537
|
-
_meta: metaFor("system"),
|
|
538
|
-
},
|
|
539
|
-
];
|
|
540
|
-
// fundi trigger — exposed only when this Worker is wired as a fundi caller
|
|
541
|
-
// (holds the M2M secret). fundi never runs in isolation: this routes the
|
|
542
|
-
// signed-in user's request to fundi with an M2M token + the user's token.
|
|
543
|
-
if (opts.fundi) {
|
|
544
|
-
coreTools.push({
|
|
545
|
-
name: `${TOOL_PREFIX}fundi_status`,
|
|
546
|
-
description: "Check fundi — the self-healing agent — on behalf of the signed-in user. fundi is machine-to-machine and only runs through an authenticated tool; this routes your request through mzizi-mcp, which holds the M2M credential.",
|
|
547
|
-
inputSchema: EMPTY_SCHEMA,
|
|
548
|
-
// Reaches the external fundi worker → open-world; still a read-only whoami.
|
|
549
|
-
annotations: annotationsFor("fundi_status", { openWorldHint: true }),
|
|
550
|
-
_meta: metaFor("fundi"),
|
|
551
|
-
});
|
|
552
|
-
// The A2A bridge (docs/a2a-fundi.md §4): delegate a run to fundi as a task
|
|
553
|
-
// and poll it. Same condition as fundi_status — a tool that always appears
|
|
554
|
-
// and always fails is worse than an absent one.
|
|
555
|
-
for (const def of FUNDI_A2A_TOOLS) {
|
|
556
|
-
coreTools.push({
|
|
557
|
-
name: `${TOOL_PREFIX}${def.name}`,
|
|
558
|
-
description: def.description,
|
|
559
|
-
inputSchema: def.inputSchema,
|
|
560
|
-
outputSchema: def.outputSchema,
|
|
561
|
-
annotations: annotationsFor(def.name, def.annotations),
|
|
562
|
-
_meta: metaFor(def.category),
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
const tools = [
|
|
567
|
-
...rows.map((r) => {
|
|
568
|
-
const emitted = emittedOutput(r.output_shape);
|
|
569
|
-
return {
|
|
570
|
-
name: `${TOOL_PREFIX}${r.tool_name}`,
|
|
571
|
-
description: r.description ??
|
|
572
|
-
`${r.category ?? "mzizi"} ${r.tool_kind ?? "read"} tool (registry-driven).`,
|
|
573
|
-
inputSchema: schemaFor(r),
|
|
574
|
-
...(emitted
|
|
575
|
-
? {
|
|
576
|
-
outputSchema: emitted.outputSchema,
|
|
577
|
-
}
|
|
578
|
-
: {}),
|
|
579
|
-
annotations: annotationsFromRow(r),
|
|
580
|
-
_meta: metaFor(r.category),
|
|
581
|
-
};
|
|
582
|
-
}),
|
|
583
|
-
...coreTools,
|
|
584
|
-
];
|
|
585
|
-
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
586
|
-
tools,
|
|
65
|
+
],
|
|
587
66
|
}));
|
|
588
|
-
server.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
? requested.slice(TOOL_PREFIX.length)
|
|
594
|
-
: requested;
|
|
595
|
-
const args = (request.params.arguments ?? {});
|
|
596
|
-
// Core document tools (aggregates without an RPC).
|
|
597
|
-
if (name === "list_collections") {
|
|
598
|
-
const { rows, error } = await fetchCollectionRows(supabase);
|
|
599
|
-
if (error)
|
|
600
|
-
return toolError(`list_collections failed: ${error}`);
|
|
601
|
-
return jsonContent(summariseCollections(rows), "rows");
|
|
602
|
-
}
|
|
603
|
-
if (name === "get_database_status") {
|
|
604
|
-
const docs = await supabase
|
|
605
|
-
.from("component_documents")
|
|
606
|
-
.select("*", { count: "exact", head: true });
|
|
607
|
-
const toolsCount = await supabase
|
|
608
|
-
.from("mcp_tool_registry")
|
|
609
|
-
.select("*", { count: "exact", head: true })
|
|
610
|
-
.eq("enabled", true);
|
|
611
|
-
return jsonContent({
|
|
612
|
-
provider: "supabase",
|
|
613
|
-
status: docs.error ? "error" : "connected",
|
|
614
|
-
error: docs.error?.message,
|
|
615
|
-
documents: docs.count ?? 0,
|
|
616
|
-
registryTools: toolsCount.count ?? 0,
|
|
617
|
-
version: MZIZI_MCP_VERSION,
|
|
618
|
-
readOnly: true,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (name === "fundi_status") {
|
|
622
|
-
if (!opts.fundi)
|
|
623
|
-
return toolError("fundi is not configured on this server");
|
|
624
|
-
const res = await opts.fundi.caller.call("/admin/whoami", opts.fundi.userAccessToken);
|
|
625
|
-
const body = await res.text();
|
|
626
|
-
if (!res.ok)
|
|
627
|
-
return toolError(`fundi call failed (${res.status}): ${body}`);
|
|
628
|
-
try {
|
|
629
|
-
return jsonContent(JSON.parse(body));
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
return jsonContent({ ok: true, raw: body });
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (isFundiA2AToolName(name)) {
|
|
636
|
-
if (!opts.fundi)
|
|
637
|
-
return toolError("fundi is not configured on this server");
|
|
638
|
-
return dispatchFundiA2A(name, args, opts.fundi);
|
|
639
|
-
}
|
|
640
|
-
const row = byName.get(name);
|
|
641
|
-
if (!row)
|
|
642
|
-
return toolError(`Unknown tool: ${name}`);
|
|
643
|
-
const wrapKey = emittedOutput(row.output_shape)?.wrapKey ?? null;
|
|
644
|
-
try {
|
|
645
|
-
if (row.sql_function) {
|
|
646
|
-
const fn = row.sql_function.replace(/^public\./, "");
|
|
647
|
-
const { data, error } = await supabase.rpc(fn, args);
|
|
648
|
-
if (error)
|
|
649
|
-
return toolError(`${row.tool_name} failed: ${error.message}`);
|
|
650
|
-
return jsonContent(data, wrapKey);
|
|
651
|
-
}
|
|
652
|
-
if (row.edge_function) {
|
|
653
|
-
const { data, error } = await supabase.functions.invoke(row.edge_function, { body: args });
|
|
654
|
-
if (error)
|
|
655
|
-
return toolError(`${row.tool_name} (edge) failed: ${error.message}`);
|
|
656
|
-
return jsonContent(data, wrapKey);
|
|
657
|
-
}
|
|
658
|
-
if (row.source_table) {
|
|
659
|
-
const { data, error } = await supabase
|
|
660
|
-
.from(row.source_table)
|
|
661
|
-
.select("*")
|
|
662
|
-
.limit(200);
|
|
663
|
-
if (error)
|
|
664
|
-
return toolError(`${row.tool_name} failed: ${error.message}`);
|
|
665
|
-
return jsonContent(data, wrapKey);
|
|
666
|
-
}
|
|
667
|
-
return toolError(`${row.tool_name} has no dispatch target in the registry.`);
|
|
668
|
-
}
|
|
669
|
-
catch (err) {
|
|
670
|
-
return toolError(`${row.tool_name} threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
// ── Resources ──────────────────────────────────────────────────────────────
|
|
674
|
-
server.resource("collections", "mzizi://collections", {
|
|
675
|
-
description: "Index of every collection in the Mzizi document store with document counts and ownership breakdown.",
|
|
676
|
-
}, async () => {
|
|
677
|
-
const { rows } = await fetchCollectionRows(supabase);
|
|
678
|
-
return {
|
|
679
|
-
contents: [
|
|
680
|
-
{
|
|
681
|
-
uri: "mzizi://collections",
|
|
682
|
-
mimeType: "application/json",
|
|
683
|
-
text: JSON.stringify(summariseCollections(rows), null, 2),
|
|
684
|
-
},
|
|
685
|
-
],
|
|
686
|
-
};
|
|
687
|
-
});
|
|
688
|
-
server.resource("components", "mzizi://components", {
|
|
689
|
-
description: "Lean index of the `components` collection (name / node / owner / status). Full documents via the get_component tool.",
|
|
690
|
-
}, async () => {
|
|
691
|
-
const { data } = await supabase.rpc("read_documents", {
|
|
692
|
-
p_collection: "components",
|
|
693
|
-
p_limit: 1000,
|
|
694
|
-
});
|
|
695
|
-
const rows = data ?? [];
|
|
696
|
-
const lean = rows.map((row) => ({
|
|
697
|
-
name: row.name,
|
|
698
|
-
owner: row.owner,
|
|
699
|
-
node: row.document?.["node"] ?? null,
|
|
700
|
-
status: row.document?.["status"] ?? null,
|
|
701
|
-
description: row.document?.["description"] ?? null,
|
|
702
|
-
}));
|
|
703
|
-
return {
|
|
704
|
-
contents: [
|
|
705
|
-
{
|
|
706
|
-
uri: "mzizi://components",
|
|
707
|
-
mimeType: "application/json",
|
|
708
|
-
text: JSON.stringify(lean, null, 2),
|
|
709
|
-
},
|
|
710
|
-
],
|
|
711
|
-
};
|
|
712
|
-
});
|
|
67
|
+
server.registerTool(`${TOOL_PREFIX}mcp_describe`, {
|
|
68
|
+
title: "Describe this server",
|
|
69
|
+
description: "What this MCP server exposes: its tools, what each replaces, and where its data comes from.",
|
|
70
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
71
|
+
}, async () => jsonContent(describe()));
|
|
713
72
|
return server;
|
|
714
73
|
}
|
|
715
|
-
/** Aggregate `(collection, node, owner)` rows into a per-collection summary. */
|
|
716
|
-
function summariseCollections(rows) {
|
|
717
|
-
const summary = new Map();
|
|
718
|
-
for (const row of rows) {
|
|
719
|
-
const entry = summary.get(row.collection) ?? {
|
|
720
|
-
collection: row.collection,
|
|
721
|
-
node: row.node,
|
|
722
|
-
total: 0,
|
|
723
|
-
byOwner: {},
|
|
724
|
-
};
|
|
725
|
-
entry.total += 1;
|
|
726
|
-
entry.byOwner[row.owner] = (entry.byOwner[row.owner] ?? 0) + 1;
|
|
727
|
-
summary.set(row.collection, entry);
|
|
728
|
-
}
|
|
729
|
-
return [...summary.values()].sort((a, b) => a.collection.localeCompare(b.collection));
|
|
730
|
-
}
|
|
731
74
|
//# sourceMappingURL=server.js.map
|