@lumibase/mcp-server 0.26.0 → 1.0.0-rc.2

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/index.js CHANGED
@@ -15,6 +15,12 @@ var LumiBaseApiError = class extends Error {
15
15
  status;
16
16
  errors;
17
17
  };
18
+ var McpUnavailableError = class extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "McpUnavailableError";
22
+ }
23
+ };
18
24
  var LumiBaseClient = class {
19
25
  baseUrl;
20
26
  origin;
@@ -56,6 +62,40 @@ var LumiBaseClient = class {
56
62
  delete(path) {
57
63
  return this.request("DELETE", path);
58
64
  }
65
+ /**
66
+ * Sends a JSON-RPC request to the governed MCP endpoint (`POST /api/v1/mcp`).
67
+ *
68
+ * Deliberately NOT built on `request()`. That helper returns `json.data`,
69
+ * which is the REST envelope — a JSON-RPC response carries `result` / `error`
70
+ * at the top level and has no `data` key at all, so routing this through
71
+ * `request()` would quietly resolve to `undefined` for every call. The bug
72
+ * would look like "governance returned nothing" rather than "we read the wrong
73
+ * field".
74
+ *
75
+ * @throws {McpUnavailableError} when the site has not enabled `contentOs.mcp`.
76
+ * @throws {LumiBaseApiError} for transport/HTTP failures and JSON-RPC errors.
77
+ */
78
+ async jsonRpc(method, params) {
79
+ const res = await fetch(`${this.baseUrl}/mcp`, {
80
+ method: "POST",
81
+ headers: this.headers,
82
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, ...params ? { params } : {} })
83
+ });
84
+ const body = await res.json().catch(() => null);
85
+ if (!res.ok) {
86
+ const errors = body?.errors ?? [{ code: "UNKNOWN", message: `HTTP ${res.status}` }];
87
+ if (errors.some((e) => e.code === "MCP_DISABLED")) {
88
+ throw new McpUnavailableError(errors[0]?.message ?? "MCP endpoint disabled");
89
+ }
90
+ throw new LumiBaseApiError(res.status, errors);
91
+ }
92
+ if (body?.error) {
93
+ throw new LumiBaseApiError(res.status, [
94
+ { code: `JSONRPC_${body.error.code}`, message: body.error.message }
95
+ ]);
96
+ }
97
+ return body?.result;
98
+ }
59
99
  /**
60
100
  * GET a root-level (non-`/api/v1`) endpoint such as `/health` or `/metrics`
61
101
  * and return the raw response body as text. These endpoints are not
@@ -108,12 +148,6 @@ function configFromEnv() {
108
148
  return { url, siteId, token };
109
149
  }
110
150
 
111
- // src/tools/access.ts
112
- import { z as z3 } from "zod";
113
-
114
- // src/tools/_crud.ts
115
- import { z as z2 } from "zod";
116
-
117
151
  // src/tools/_shared.ts
118
152
  function formatError(err) {
119
153
  if (err instanceof LumiBaseApiError) {
@@ -153,6 +187,273 @@ async function run(fn) {
153
187
  }
154
188
  }
155
189
  var confirmDescription = "Must be true to confirm the destructive operation";
190
+ var DECISION_STATUSES = /* @__PURE__ */ new Set(["executed", "pending_approval", "denied"]);
191
+ function asGovernedDecision(value) {
192
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
193
+ const status = value.status;
194
+ if (typeof status !== "string" || !DECISION_STATUSES.has(status)) return void 0;
195
+ return value;
196
+ }
197
+ function renderDecision(decision, executedText) {
198
+ if (decision.status === "executed") {
199
+ return decision.data === void 0 ? okText(executedText) : ok(decision.data);
200
+ }
201
+ if (decision.status === "pending_approval") {
202
+ const id = decision.agentApprovalId ?? decision.approvalId;
203
+ const space = decision.approvalSpace ?? (decision.agentApprovalId ? "agent" : id ? "legacy_ai" : void 0);
204
+ const endpoint = space === "agent" ? "POST /api/v1/agent/approvals/{approvalId}/decide" : space === "legacy_ai" ? "POST /api/v1/ai/approvals/{approvalId}/decide" : void 0;
205
+ const lines = [
206
+ "Not executed \u2014 pending approval.",
207
+ ...id ? [`Approval id: ${id}`] : [],
208
+ ...endpoint ? [`Decide at: ${endpoint}`] : [],
209
+ ...decision.runId ? [`Run: ${decision.runId}`] : [],
210
+ ...decision.message ? [decision.message] : []
211
+ ];
212
+ return okText(lines.join("\n"));
213
+ }
214
+ const reason = decision.code ? `[${decision.code}] ` : "";
215
+ return {
216
+ content: [{ type: "text", text: `Not executed \u2014 denied. ${reason}${decision.message ?? ""}`.trim() }],
217
+ isError: true
218
+ };
219
+ }
220
+ function okAfter(response, executedText) {
221
+ const decision = asGovernedDecision(response);
222
+ return decision === void 0 ? okText(executedText) : renderDecision(decision, executedText);
223
+ }
224
+
225
+ // src/governed.ts
226
+ var GOVERNED_TOOLS = {
227
+ // ── items ────────────────────────────────────────────────────────────────
228
+ create_item: { skill: "createItem" },
229
+ update_item: { skill: "updateItem" },
230
+ delete_item: { skill: "deleteItem" },
231
+ // ── schema ───────────────────────────────────────────────────────────────
232
+ delete_collection: { skill: "deleteCollection" },
233
+ // `field_name` is this transport's name for the field; the skill reads `name`.
234
+ // Renamed explicitly rather than dropped (see repro R17).
235
+ delete_field: { skill: "deleteField", rename: { field_name: "name" } },
236
+ delete_relation: { skill: "deleteRelation" },
237
+ // ── access ───────────────────────────────────────────────────────────────
238
+ delete_role: { skill: "deleteRole" },
239
+ delete_policy: { skill: "deletePolicy" },
240
+ // ── automation ───────────────────────────────────────────────────────────
241
+ delete_flow: { skill: "deleteFlow" },
242
+ run_flow: { skill: "runFlow" },
243
+ delete_intent: { skill: "deleteIntent" },
244
+ // ── config ───────────────────────────────────────────────────────────────
245
+ upsert_setting: { skill: "upsertSetting" },
246
+ delete_setting: { skill: "deleteSetting" },
247
+ create_translation: { skill: "createTranslation" },
248
+ delete_translation: { skill: "deleteTranslation" },
249
+ delete_webhook: { skill: "deleteWebhook" },
250
+ // ── api keys / users / teams ──────────────────────────────────────────────
251
+ create_api_key: { skill: "createApiKey" },
252
+ rotate_api_key: { skill: "rotateApiKey" },
253
+ revoke_api_key: { skill: "revokeApiKey" },
254
+ invite_user: { skill: "inviteUser" },
255
+ update_user: { skill: "updateUser" },
256
+ remove_user: { skill: "removeUser" },
257
+ create_team: { skill: "createTeam" },
258
+ delete_team: { skill: "deleteTeam" },
259
+ // The team tools take the team as `id`; the skill names it `teamId` because it
260
+ // also takes a `userId` and one bare `id` would be ambiguous.
261
+ add_team_member: { skill: "addTeamMember", rename: { id: "teamId" } },
262
+ remove_team_member: { skill: "removeTeamMember", rename: { id: "teamId" } },
263
+ // ── extensions ───────────────────────────────────────────────────────────
264
+ uninstall_extension: { skill: "uninstallExtension" }
265
+ };
266
+ var UNGOVERNED_MUTATIONS = {
267
+ // Skill exists, but the canonical contract is narrower than what this tool
268
+ // advertises. Routing now would reject arguments callers legitimately send.
269
+ create_collection: "contract-narrower-than-tool: 16 advertised properties have no canonical counterpart",
270
+ create_policy: "contract-narrower-than-tool: enforceTfa/ipAllow/ipDeny/validFrom/validUntil",
271
+ create_role: "contract-narrower-than-tool: systemKey",
272
+ cdc_subscription_replay: "contract-narrower-than-tool: cursor has no canonical counterpart",
273
+ // Skill exists but has no canonical input contract yet.
274
+ create_cdc_subscription: "no-canonical-contract",
275
+ delete_cdc_subscription: "no-canonical-contract",
276
+ create_flow: "no-canonical-contract",
277
+ create_intent: "no-canonical-contract",
278
+ create_relation: "no-canonical-contract",
279
+ create_webhook: "no-canonical-contract",
280
+ update_webhook: "no-canonical-contract",
281
+ update_translation: "no-canonical-contract",
282
+ install_extension: "no-canonical-contract",
283
+ update_extension: "no-canonical-contract",
284
+ // No skill at all: nothing to route to. Listed so the set is closed.
285
+ add_policy_permission: "no-skill",
286
+ apply_access_import: "no-skill",
287
+ apply_schema: "no-skill",
288
+ approve_content: "no-skill",
289
+ assign_role_user: "no-skill",
290
+ attach_api_key_policy: "no-skill",
291
+ attach_api_key_role: "no-skill",
292
+ attach_policy_user: "no-skill",
293
+ attach_role_policy: "no-skill",
294
+ compile_intent: "no-skill",
295
+ create_preset: "no-skill",
296
+ create_release: "no-skill",
297
+ create_share: "no-skill",
298
+ delete_media: "no-skill",
299
+ delete_policy_permission: "no-skill",
300
+ delete_preset: "no-skill",
301
+ delete_release: "no-skill",
302
+ delete_tm: "no-skill",
303
+ detach_api_key_policy: "no-skill",
304
+ detach_api_key_role: "no-skill",
305
+ detach_policy_user: "no-skill",
306
+ detach_role_policy: "no-skill",
307
+ drop_materialization: "no-skill",
308
+ install_marketplace_extension: "no-skill",
309
+ publish_extension: "no-skill",
310
+ publish_release: "no-skill",
311
+ refresh_materialization: "no-skill",
312
+ reject_content: "no-skill",
313
+ remove_role_user: "no-skill",
314
+ restore_backup: "no-skill",
315
+ revoke_share: "no-skill",
316
+ run_panel: "no-skill",
317
+ submit_review: "no-skill",
318
+ translate_text: "no-skill",
319
+ update_cdc_subscription: "no-skill",
320
+ update_collection: "no-skill",
321
+ update_flow: "no-skill",
322
+ update_intent: "no-skill",
323
+ update_policy: "no-skill",
324
+ update_policy_permission: "no-skill",
325
+ update_preset: "no-skill",
326
+ update_release: "no-skill",
327
+ update_role: "no-skill",
328
+ update_team: "no-skill",
329
+ update_tm: "no-skill",
330
+ upsert_field: "no-skill",
331
+ upsert_tm: "no-skill"
332
+ };
333
+ var MUTATION_VERB = /^(create|update|delete|remove|upsert|set|add|attach|detach|revoke|rotate|invite|install|uninstall|enable|disable|publish|unpublish|promote|apply|run|trigger|restore|replay|drop|materialize|reset|assign|unassign|approve|reject|submit|claim|decide|import|veto|freeze|lift|seed|sync|purge|bump|stage|commit|schedule|cancel|retry|archive|clone|duplicate|move|rename|reorder|translate|compile|generate|refresh|configure)/;
334
+ var MUTATION_EXCEPTIONS = /* @__PURE__ */ new Set(["cdc_subscription_replay"]);
335
+ function isMutationTool(name) {
336
+ return MUTATION_EXCEPTIONS.has(name) || MUTATION_VERB.test(name);
337
+ }
338
+ var PROMPT_ONLY_ARGS = /* @__PURE__ */ new Set(["confirm"]);
339
+ var camel = (key) => key.replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
340
+ function toSkillArgs(args, binding) {
341
+ const out = {};
342
+ for (const [key, value] of Object.entries(args)) {
343
+ if (PROMPT_ONLY_ARGS.has(key)) continue;
344
+ if (value === void 0) continue;
345
+ out[binding.rename?.[key] ?? camel(key)] = value;
346
+ }
347
+ return out;
348
+ }
349
+ function governedModeFromEnv(env = process.env) {
350
+ const raw = (env["LUMIBASE_MCP_GOVERNED"] ?? "auto").toLowerCase();
351
+ if (raw === "true" || raw === "on" || raw === "1") return "on";
352
+ if (raw === "false" || raw === "off" || raw === "0") return "off";
353
+ return "auto";
354
+ }
355
+ var GovernedDispatcher = class {
356
+ constructor(client, options = {}) {
357
+ this.client = client;
358
+ this.mode = options.mode ?? governedModeFromEnv();
359
+ this.warn = options.warn ?? ((message) => console.error(message));
360
+ }
361
+ client;
362
+ mode;
363
+ warn;
364
+ available;
365
+ get enabled() {
366
+ return this.mode !== "off";
367
+ }
368
+ /**
369
+ * True when this deployment requires every mutation to go through governance.
370
+ *
371
+ * Read by the registration wrapper to refuse mutations that have no governed
372
+ * mapping. Without it, mode `on` only governed the 27 mapped tools and left the
373
+ * rest on REST — so the setting that exists to guarantee governance did not,
374
+ * and the guarantee failed silently for exactly the calls nobody had mapped
375
+ * yet.
376
+ */
377
+ get requiresGovernance() {
378
+ return this.mode === "on";
379
+ }
380
+ /**
381
+ * The refusal returned for a mutation that cannot be governed.
382
+ *
383
+ * Names the reason from {@link UNGOVERNED_MUTATIONS} when there is one, so the
384
+ * operator can tell "we know about this gap" from "nobody classified this tool".
385
+ */
386
+ refuseUngoverned(tool) {
387
+ const reason = UNGOVERNED_MUTATIONS[tool];
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text",
392
+ text: `Refused: "${tool}" changes state but has no governed mapping, and LUMIBASE_MCP_GOVERNED=on requires every mutation to run through the agent harness (autonomy levels, HITL approval, kill switch, run audit).
393
+ ` + (reason ? `Known gap: ${reason}.
394
+ ` : "This tool is in neither the governed nor the declared-ungoverned table, which means it was added without a governance decision.\n") + "Set LUMIBASE_MCP_GOVERNED=auto or off to accept ungoverned REST calls for it."
395
+ }
396
+ ],
397
+ isError: true
398
+ };
399
+ }
400
+ /** True when the governed endpoint answered a probe. */
401
+ probe() {
402
+ this.available ??= (async () => {
403
+ try {
404
+ await this.client.jsonRpc("tools/list");
405
+ return true;
406
+ } catch (err) {
407
+ if (this.mode === "on") return false;
408
+ this.warn(
409
+ err instanceof McpUnavailableError ? "[lumibase-mcp] governed tool calls unavailable: this site has contentOs.mcp disabled. Falling back to direct REST calls, which skip agent governance (autonomy levels, HITL approval, kill switch, run audit). Set LUMIBASE_MCP_GOVERNED=on to refuse instead of falling back." : `[lumibase-mcp] governed endpoint probe failed (${err instanceof Error ? err.message : String(err)}); falling back to direct REST calls, which skip agent governance. Set LUMIBASE_MCP_GOVERNED=on to refuse instead of falling back.`
410
+ );
411
+ return false;
412
+ }
413
+ })();
414
+ return this.available;
415
+ }
416
+ /**
417
+ * Runs `tool` through the harness.
418
+ *
419
+ * @returns the rendered tool result, or `undefined` when the caller should run
420
+ * its own REST handler instead (mode `auto` with governance unavailable).
421
+ */
422
+ async dispatch(tool, args, binding) {
423
+ if (!this.enabled) return void 0;
424
+ if (!await this.probe()) {
425
+ if (this.mode === "on") {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: `Refused: "${tool}" must run through the governed harness, but this site has contentOs.mcp disabled. Enable it, or set LUMIBASE_MCP_GOVERNED=off to accept ungoverned REST calls.`
431
+ }
432
+ ],
433
+ isError: true
434
+ };
435
+ }
436
+ return void 0;
437
+ }
438
+ try {
439
+ const result = await this.client.jsonRpc("tools/call", { name: binding.skill, arguments: toSkillArgs(args, binding) });
440
+ const decision = asGovernedDecision(result?.structuredContent);
441
+ if (decision) return renderDecision(decision, `${tool} executed.`);
442
+ return {
443
+ content: result?.content ?? [{ type: "text", text: JSON.stringify(result ?? null, null, 2) }],
444
+ ...result?.isError === void 0 ? {} : { isError: result.isError }
445
+ };
446
+ } catch (err) {
447
+ return fail(err);
448
+ }
449
+ }
450
+ };
451
+
452
+ // src/tools/access.ts
453
+ import { z as z3 } from "zod";
454
+
455
+ // src/tools/_crud.ts
456
+ import { z as z2 } from "zod";
156
457
 
157
458
  // src/tools/path.ts
158
459
  import { z } from "zod";
@@ -239,8 +540,8 @@ function registerCrud(server, client, opts) {
239
540
  async (args) => {
240
541
  const id = String(args[idParam]);
241
542
  return run(async () => {
242
- await client.delete(`${basePath}/${encodePathSegment(id)}`);
243
- return okText(`${resource} "${id}" deleted.`);
543
+ const response = await client.delete(`${basePath}/${encodePathSegment(id)}`);
544
+ return okAfter(response, `${resource} "${id}" deleted.`);
244
545
  });
245
546
  }
246
547
  );
@@ -312,8 +613,8 @@ function registerAccessTools(server, client) {
312
613
  }
313
614
  },
314
615
  async ({ id, policyId }) => run(async () => {
315
- await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
316
- return okText(`Policy "${policyId}" detached from role "${id}".`);
616
+ const response = await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
617
+ return okAfter(response, `Policy "${policyId}" detached from role "${id}".`);
317
618
  })
318
619
  );
319
620
  server.registerTool(
@@ -335,8 +636,8 @@ function registerAccessTools(server, client) {
335
636
  }
336
637
  },
337
638
  async ({ id, userId }) => run(async () => {
338
- await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
339
- return okText(`User "${userId}" removed from role "${id}".`);
639
+ const response = await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
640
+ return okAfter(response, `User "${userId}" removed from role "${id}".`);
340
641
  })
341
642
  );
342
643
  registerCrud(server, client, {
@@ -378,8 +679,8 @@ function registerAccessTools(server, client) {
378
679
  }
379
680
  },
380
681
  async ({ id, permId }) => run(async () => {
381
- await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
382
- return okText(`Permission "${permId}" deleted from policy "${id}".`);
682
+ const response = await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
683
+ return okAfter(response, `Permission "${permId}" deleted from policy "${id}".`);
383
684
  })
384
685
  );
385
686
  server.registerTool(
@@ -406,8 +707,8 @@ function registerAccessTools(server, client) {
406
707
  }
407
708
  },
408
709
  async ({ id, userId }) => run(async () => {
409
- await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
410
- return okText(`Policy "${id}" detached from user "${userId}".`);
710
+ const response = await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
711
+ return okAfter(response, `Policy "${id}" detached from user "${userId}".`);
411
712
  })
412
713
  );
413
714
  server.registerTool(
@@ -515,8 +816,8 @@ function registerAdminTools(server, client) {
515
816
  inputSchema: { id: idPathSegmentSchema, confirm: z4.literal(true).describe(confirmDescription) }
516
817
  },
517
818
  async ({ id }) => run(async () => {
518
- await client.delete(`/materialize/${encodePathSegment(id)}`);
519
- return okText(`Materialization "${id}" dropped.`);
819
+ const response = await client.delete(`/materialize/${encodePathSegment(id)}`);
820
+ return okAfter(response, `Materialization "${id}" dropped.`);
520
821
  })
521
822
  );
522
823
  }
@@ -692,8 +993,8 @@ function registerApiKeyTools(server, client) {
692
993
  }
693
994
  },
694
995
  async ({ id, roleId }) => run(async () => {
695
- await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
696
- return okText(`Role "${roleId}" detached from API key "${id}".`);
996
+ const response = await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
997
+ return okAfter(response, `Role "${roleId}" detached from API key "${id}".`);
697
998
  })
698
999
  );
699
1000
  server.registerTool(
@@ -720,8 +1021,8 @@ function registerApiKeyTools(server, client) {
720
1021
  }
721
1022
  },
722
1023
  async ({ id, policyId }) => run(async () => {
723
- await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
724
- return okText(`Policy "${policyId}" detached from API key "${id}".`);
1024
+ const response = await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
1025
+ return okAfter(response, `Policy "${policyId}" detached from API key "${id}".`);
725
1026
  })
726
1027
  );
727
1028
  }
@@ -913,8 +1214,8 @@ function registerCollectionTools(server, client) {
913
1214
  },
914
1215
  async ({ name, confirm: _ }) => {
915
1216
  try {
916
- await client.delete(`/collections/${encodePathSegment(name)}`);
917
- return { content: [{ type: "text", text: `Collection "${name}" deleted.` }] };
1217
+ const response = await client.delete(`/collections/${encodePathSegment(name)}`);
1218
+ return okAfter(response, `Collection "${name}" deleted.`);
918
1219
  } catch (err) {
919
1220
  return { content: [{ type: "text", text: `Error: ${formatError2(err)}` }], isError: true };
920
1221
  }
@@ -1070,8 +1371,8 @@ function registerContentConfigTools(server, client) {
1070
1371
  }
1071
1372
  },
1072
1373
  async ({ key }) => run(async () => {
1073
- await client.delete(`/settings/${encodePathSegment(key)}`);
1074
- return okText(`Setting "${key}" deleted.`);
1374
+ const response = await client.delete(`/settings/${encodePathSegment(key)}`);
1375
+ return okAfter(response, `Setting "${key}" deleted.`);
1075
1376
  })
1076
1377
  );
1077
1378
  }
@@ -1234,8 +1535,8 @@ function registerExtensionTools(server, client) {
1234
1535
  inputSchema: { id: idPathSegmentSchema, confirm: z12.literal(true).describe(confirmDescription) }
1235
1536
  },
1236
1537
  async ({ id }) => run(async () => {
1237
- await client.delete(`/extensions/${encodePathSegment(id)}`);
1238
- return okText(`Extension "${id}" uninstalled.`);
1538
+ const response = await client.delete(`/extensions/${encodePathSegment(id)}`);
1539
+ return okAfter(response, `Extension "${id}" uninstalled.`);
1239
1540
  })
1240
1541
  );
1241
1542
  server.registerTool(
@@ -1391,12 +1692,10 @@ function registerFieldTools(server, client) {
1391
1692
  async ({ collection, field_name, force }) => {
1392
1693
  try {
1393
1694
  const qs = force ? "?force=true" : "";
1394
- await client.delete(
1695
+ const response = await client.delete(
1395
1696
  `/collections/${encodePathSegment(collection)}/fields/${encodePathSegment(field_name)}${qs}`
1396
1697
  );
1397
- return {
1398
- content: [{ type: "text", text: `Field "${field_name}" deleted from "${collection}".` }]
1399
- };
1698
+ return okAfter(response, `Field "${field_name}" deleted from "${collection}".`);
1400
1699
  } catch (err) {
1401
1700
  return { content: [{ type: "text", text: `Error: ${formatError3(err)}` }], isError: true };
1402
1701
  }
@@ -1563,7 +1862,7 @@ function registerItemTools(server, client) {
1563
1862
  async ({ collection, data: itemData, status }) => {
1564
1863
  try {
1565
1864
  const data = await client.post(`/items/${encodePathSegment(collection)}`, {
1566
- ...itemData,
1865
+ data: itemData,
1567
1866
  status
1568
1867
  });
1569
1868
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -1586,7 +1885,7 @@ function registerItemTools(server, client) {
1586
1885
  try {
1587
1886
  const data = await client.patch(
1588
1887
  `/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`,
1589
- itemData
1888
+ { data: itemData }
1590
1889
  );
1591
1890
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1592
1891
  } catch (err) {
@@ -1606,8 +1905,10 @@ function registerItemTools(server, client) {
1606
1905
  },
1607
1906
  async ({ collection, id }) => {
1608
1907
  try {
1609
- await client.delete(`/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`);
1610
- return { content: [{ type: "text", text: `Item "${id}" deleted from "${collection}".` }] };
1908
+ const response = await client.delete(
1909
+ `/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`
1910
+ );
1911
+ return okAfter(response, `Item "${id}" deleted from "${collection}".`);
1611
1912
  } catch (err) {
1612
1913
  return { content: [{ type: "text", text: `Error: ${formatError4(err)}` }], isError: true };
1613
1914
  }
@@ -1695,8 +1996,8 @@ function registerReleaseTools(server, client) {
1695
1996
  }
1696
1997
  },
1697
1998
  async ({ id }) => run(async () => {
1698
- await client.delete(`/releases/${encodePathSegment(id)}`);
1699
- return okText(`Release "${id}" deleted.`);
1999
+ const response = await client.delete(`/releases/${encodePathSegment(id)}`);
2000
+ return okAfter(response, `Release "${id}" deleted.`);
1700
2001
  })
1701
2002
  );
1702
2003
  }
@@ -1730,8 +2031,8 @@ function registerShareTools(server, client) {
1730
2031
  }
1731
2032
  },
1732
2033
  async ({ id }) => run(async () => {
1733
- await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
1734
- return okText(`Share link "${id}" revoked.`);
2034
+ const response = await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
2035
+ return okAfter(response, `Share link "${id}" revoked.`);
1735
2036
  })
1736
2037
  );
1737
2038
  }
@@ -1840,8 +2141,8 @@ function registerRelationTools(server, client) {
1840
2141
  }
1841
2142
  },
1842
2143
  async ({ id }) => run(async () => {
1843
- await client.delete(`/relations/${encodePathSegment(id)}`);
1844
- return okText(`Relation "${id}" deleted.`);
2144
+ const response = await client.delete(`/relations/${encodePathSegment(id)}`);
2145
+ return okAfter(response, `Relation "${id}" deleted.`);
1845
2146
  })
1846
2147
  );
1847
2148
  }
@@ -1886,8 +2187,8 @@ function registerSearchMediaTools(server, client) {
1886
2187
  }
1887
2188
  },
1888
2189
  async ({ key }) => run(async () => {
1889
- await client.delete(`/media/${encodeMediaKey(key)}`);
1890
- return okText(`Media asset "${key}" deleted.`);
2190
+ const response = await client.delete(`/media/${encodeMediaKey(key)}`);
2191
+ return okAfter(response, `Media asset "${key}" deleted.`);
1891
2192
  })
1892
2193
  );
1893
2194
  server.registerTool(
@@ -1983,8 +2284,8 @@ function registerTranslationMemoryTools(server, client) {
1983
2284
  }
1984
2285
  },
1985
2286
  async ({ id }) => run(async () => {
1986
- await client.delete(`/tm/${encodePathSegment(id)}`);
1987
- return okText(`Translation-memory entry "${id}" deleted.`);
2287
+ const response = await client.delete(`/tm/${encodePathSegment(id)}`);
2288
+ return okAfter(response, `Translation-memory entry "${id}" deleted.`);
1988
2289
  })
1989
2290
  );
1990
2291
  }
@@ -2032,8 +2333,8 @@ function registerUsersTeamsTools(server, client) {
2032
2333
  inputSchema: { id: idPathSegmentSchema, confirm: z23.literal(true).describe(confirmDescription) }
2033
2334
  },
2034
2335
  async ({ id }) => run(async () => {
2035
- await client.delete(`/users/${encodePathSegment(id)}`);
2036
- return okText(`User "${id}" removed from the site.`);
2336
+ const response = await client.delete(`/users/${encodePathSegment(id)}`);
2337
+ return okAfter(response, `User "${id}" removed from the site.`);
2037
2338
  })
2038
2339
  );
2039
2340
  server.registerTool(
@@ -2073,8 +2374,8 @@ function registerUsersTeamsTools(server, client) {
2073
2374
  inputSchema: { id: idPathSegmentSchema, confirm: z23.literal(true).describe(confirmDescription) }
2074
2375
  },
2075
2376
  async ({ id }) => run(async () => {
2076
- await client.delete(`/teams/${encodePathSegment(id)}`);
2077
- return okText(`Team "${id}" deleted.`);
2377
+ const response = await client.delete(`/teams/${encodePathSegment(id)}`);
2378
+ return okAfter(response, `Team "${id}" deleted.`);
2078
2379
  })
2079
2380
  );
2080
2381
  server.registerTool(
@@ -2101,8 +2402,8 @@ function registerUsersTeamsTools(server, client) {
2101
2402
  }
2102
2403
  },
2103
2404
  async ({ id, userId }) => run(async () => {
2104
- await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
2105
- return okText(`User "${userId}" removed from team "${id}".`);
2405
+ const response = await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
2406
+ return okAfter(response, `User "${userId}" removed from team "${id}".`);
2106
2407
  })
2107
2408
  );
2108
2409
  }
@@ -2131,7 +2432,33 @@ function registerWebhookTools(server, client) {
2131
2432
  }
2132
2433
 
2133
2434
  // src/tools/index.ts
2134
- function registerAllTools(server, client) {
2435
+ function registerAllTools(server, client, options = {}) {
2436
+ const dispatcher = options.dispatcher === void 0 ? new GovernedDispatcher(client) : options.dispatcher;
2437
+ const target = dispatcher?.enabled ? withGovernedHandlers(server, dispatcher) : server;
2438
+ registerModules(target, client);
2439
+ }
2440
+ function withGovernedHandlers(server, dispatcher) {
2441
+ return new Proxy(server, {
2442
+ get(t, prop, receiver) {
2443
+ if (prop !== "registerTool") return Reflect.get(t, prop, receiver);
2444
+ return (name, config, handler) => {
2445
+ const binding = GOVERNED_TOOLS[name];
2446
+ let wrapped = handler;
2447
+ if (binding) {
2448
+ wrapped = async (args) => await dispatcher.dispatch(name, args, binding) ?? await handler(args);
2449
+ } else if (dispatcher.requiresGovernance && isMutationTool(name)) {
2450
+ wrapped = async () => dispatcher.refuseUngoverned(name);
2451
+ }
2452
+ return t.registerTool(
2453
+ name,
2454
+ config,
2455
+ wrapped
2456
+ );
2457
+ };
2458
+ }
2459
+ });
2460
+ }
2461
+ function registerModules(server, client) {
2135
2462
  registerCollectionTools(server, client);
2136
2463
  registerFieldTools(server, client);
2137
2464
  registerItemTools(server, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumibase/mcp-server",
3
- "version": "0.26.0",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "LumiBase MCP server — lets AI assistants manage the full LumiBase Content OS: collections, fields, items, relations, RBAC (roles/policies/permissions/API keys), users & teams, intents, flows, webhooks, translations & translation memory, search, media & transform presets, read-only insights, presets, editorial workflow, content releases, share links, read-only deployments, extensions, and site administration.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@modelcontextprotocol/sdk": "^1.30.0",
30
- "zod": "^4.4.3"
30
+ "zod": "^4.5.4"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^26.2.0",
33
+ "@types/node": "^26.4.1",
34
34
  "tsup": "^8.5.1",
35
35
  "typescript": "^5.6.2",
36
- "vitest": "^4.1.10"
36
+ "vitest": "^5.0.0"
37
37
  },
38
38
  "files": [
39
39
  "dist"
@@ -41,6 +41,20 @@
41
41
  "engines": {
42
42
  "node": ">=18"
43
43
  },
44
+ "homepage": "https://docs.lumibase.dev",
45
+ "bugs": {
46
+ "url": "https://github.com/khuepm/lumibase/issues"
47
+ },
48
+ "keywords": [
49
+ "lumibase",
50
+ "cms",
51
+ "mcp",
52
+ "model-context-protocol",
53
+ "ai",
54
+ "claude",
55
+ "cursor",
56
+ "stdio"
57
+ ],
44
58
  "scripts": {
45
59
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
46
60
  "dev": "tsup src/index.ts --format cjs --watch",