@lumibase/mcp-server 1.0.0-rc.1 → 1.0.0-rc.3
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.cjs +392 -53
- package/dist/index.js +392 -53
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -16,6 +16,12 @@ var LumiBaseApiError = class extends Error {
|
|
|
16
16
|
status;
|
|
17
17
|
errors;
|
|
18
18
|
};
|
|
19
|
+
var McpUnavailableError = class extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "McpUnavailableError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
19
25
|
var LumiBaseClient = class {
|
|
20
26
|
baseUrl;
|
|
21
27
|
origin;
|
|
@@ -57,6 +63,40 @@ var LumiBaseClient = class {
|
|
|
57
63
|
delete(path) {
|
|
58
64
|
return this.request("DELETE", path);
|
|
59
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Sends a JSON-RPC request to the governed MCP endpoint (`POST /api/v1/mcp`).
|
|
68
|
+
*
|
|
69
|
+
* Deliberately NOT built on `request()`. That helper returns `json.data`,
|
|
70
|
+
* which is the REST envelope — a JSON-RPC response carries `result` / `error`
|
|
71
|
+
* at the top level and has no `data` key at all, so routing this through
|
|
72
|
+
* `request()` would quietly resolve to `undefined` for every call. The bug
|
|
73
|
+
* would look like "governance returned nothing" rather than "we read the wrong
|
|
74
|
+
* field".
|
|
75
|
+
*
|
|
76
|
+
* @throws {McpUnavailableError} when the site has not enabled `contentOs.mcp`.
|
|
77
|
+
* @throws {LumiBaseApiError} for transport/HTTP failures and JSON-RPC errors.
|
|
78
|
+
*/
|
|
79
|
+
async jsonRpc(method, params) {
|
|
80
|
+
const res = await fetch(`${this.baseUrl}/mcp`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: this.headers,
|
|
83
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, ...params ? { params } : {} })
|
|
84
|
+
});
|
|
85
|
+
const body = await res.json().catch(() => null);
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const errors = body?.errors ?? [{ code: "UNKNOWN", message: `HTTP ${res.status}` }];
|
|
88
|
+
if (errors.some((e) => e.code === "MCP_DISABLED")) {
|
|
89
|
+
throw new McpUnavailableError(errors[0]?.message ?? "MCP endpoint disabled");
|
|
90
|
+
}
|
|
91
|
+
throw new LumiBaseApiError(res.status, errors);
|
|
92
|
+
}
|
|
93
|
+
if (body?.error) {
|
|
94
|
+
throw new LumiBaseApiError(res.status, [
|
|
95
|
+
{ code: `JSONRPC_${body.error.code}`, message: body.error.message }
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
return body?.result;
|
|
99
|
+
}
|
|
60
100
|
/**
|
|
61
101
|
* GET a root-level (non-`/api/v1`) endpoint such as `/health` or `/metrics`
|
|
62
102
|
* and return the raw response body as text. These endpoints are not
|
|
@@ -109,12 +149,6 @@ function configFromEnv() {
|
|
|
109
149
|
return { url, siteId, token };
|
|
110
150
|
}
|
|
111
151
|
|
|
112
|
-
// src/tools/access.ts
|
|
113
|
-
var import_zod3 = require("zod");
|
|
114
|
-
|
|
115
|
-
// src/tools/_crud.ts
|
|
116
|
-
var import_zod2 = require("zod");
|
|
117
|
-
|
|
118
152
|
// src/tools/_shared.ts
|
|
119
153
|
function formatError(err) {
|
|
120
154
|
if (err instanceof LumiBaseApiError) {
|
|
@@ -154,6 +188,285 @@ async function run(fn) {
|
|
|
154
188
|
}
|
|
155
189
|
}
|
|
156
190
|
var confirmDescription = "Must be true to confirm the destructive operation";
|
|
191
|
+
var DECISION_STATUSES = /* @__PURE__ */ new Set(["executed", "pending_approval", "denied"]);
|
|
192
|
+
function asGovernedDecision(value) {
|
|
193
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
194
|
+
const status = value.status;
|
|
195
|
+
if (typeof status !== "string" || !DECISION_STATUSES.has(status)) return void 0;
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
function renderDecision(decision, executedText) {
|
|
199
|
+
if (decision.status === "executed") {
|
|
200
|
+
return decision.data === void 0 ? okText(executedText) : ok(decision.data);
|
|
201
|
+
}
|
|
202
|
+
if (decision.status === "pending_approval") {
|
|
203
|
+
const id = decision.agentApprovalId ?? decision.approvalId;
|
|
204
|
+
const space = decision.approvalSpace ?? (decision.agentApprovalId ? "agent" : id ? "legacy_ai" : void 0);
|
|
205
|
+
const endpoint = space === "agent" ? "POST /api/v1/agent/approvals/{approvalId}/decide" : space === "legacy_ai" ? "POST /api/v1/ai/approvals/{approvalId}/decide" : void 0;
|
|
206
|
+
const lines = [
|
|
207
|
+
"Not executed \u2014 pending approval.",
|
|
208
|
+
...id ? [`Approval id: ${id}`] : [],
|
|
209
|
+
...endpoint ? [`Decide at: ${endpoint}`] : [],
|
|
210
|
+
...decision.runId ? [`Run: ${decision.runId}`] : [],
|
|
211
|
+
...decision.message ? [decision.message] : []
|
|
212
|
+
];
|
|
213
|
+
return okText(lines.join("\n"));
|
|
214
|
+
}
|
|
215
|
+
const reason = decision.code ? `[${decision.code}] ` : "";
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: `Not executed \u2014 denied. ${reason}${decision.message ?? ""}`.trim() }],
|
|
218
|
+
isError: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function okAfter(response, executedText) {
|
|
222
|
+
const decision = asGovernedDecision(response);
|
|
223
|
+
return decision === void 0 ? okText(executedText) : renderDecision(decision, executedText);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/governed.ts
|
|
227
|
+
var GOVERNED_TOOLS = {
|
|
228
|
+
// ── items ────────────────────────────────────────────────────────────────
|
|
229
|
+
create_item: { skill: "createItem" },
|
|
230
|
+
update_item: { skill: "updateItem" },
|
|
231
|
+
delete_item: { skill: "deleteItem" },
|
|
232
|
+
// ── schema ───────────────────────────────────────────────────────────────
|
|
233
|
+
delete_collection: { skill: "deleteCollection" },
|
|
234
|
+
// `field_name` is this transport's name for the field; the skill reads `name`.
|
|
235
|
+
// Renamed explicitly rather than dropped (see repro R17).
|
|
236
|
+
delete_field: { skill: "deleteField", rename: { field_name: "name" } },
|
|
237
|
+
create_relation: { skill: "createRelation" },
|
|
238
|
+
delete_relation: { skill: "deleteRelation" },
|
|
239
|
+
// ── access ───────────────────────────────────────────────────────────────
|
|
240
|
+
delete_role: { skill: "deleteRole" },
|
|
241
|
+
delete_policy: { skill: "deletePolicy" },
|
|
242
|
+
// ── automation ───────────────────────────────────────────────────────────
|
|
243
|
+
delete_flow: { skill: "deleteFlow" },
|
|
244
|
+
run_flow: { skill: "runFlow" },
|
|
245
|
+
create_intent: { skill: "createIntent" },
|
|
246
|
+
delete_intent: { skill: "deleteIntent" },
|
|
247
|
+
// ── config ───────────────────────────────────────────────────────────────
|
|
248
|
+
upsert_setting: { skill: "upsertSetting" },
|
|
249
|
+
delete_setting: { skill: "deleteSetting" },
|
|
250
|
+
create_translation: { skill: "createTranslation" },
|
|
251
|
+
update_translation: { skill: "updateTranslation" },
|
|
252
|
+
delete_translation: { skill: "deleteTranslation" },
|
|
253
|
+
create_webhook: { skill: "createWebhook" },
|
|
254
|
+
update_webhook: { skill: "updateWebhook" },
|
|
255
|
+
delete_webhook: { skill: "deleteWebhook" },
|
|
256
|
+
// ── api keys / users / teams ──────────────────────────────────────────────
|
|
257
|
+
create_api_key: { skill: "createApiKey" },
|
|
258
|
+
rotate_api_key: { skill: "rotateApiKey" },
|
|
259
|
+
revoke_api_key: { skill: "revokeApiKey" },
|
|
260
|
+
invite_user: { skill: "inviteUser" },
|
|
261
|
+
update_user: { skill: "updateUser" },
|
|
262
|
+
remove_user: { skill: "removeUser" },
|
|
263
|
+
create_team: { skill: "createTeam" },
|
|
264
|
+
delete_team: { skill: "deleteTeam" },
|
|
265
|
+
// The team tools take the team as `id`; the skill names it `teamId` because it
|
|
266
|
+
// also takes a `userId` and one bare `id` would be ambiguous.
|
|
267
|
+
add_team_member: { skill: "addTeamMember", rename: { id: "teamId" } },
|
|
268
|
+
remove_team_member: { skill: "removeTeamMember", rename: { id: "teamId" } },
|
|
269
|
+
// ── extensions ───────────────────────────────────────────────────────────
|
|
270
|
+
uninstall_extension: { skill: "uninstallExtension" },
|
|
271
|
+
// ── cdc ──────────────────────────────────────────────────────────────────
|
|
272
|
+
// The change-feed skills name the subscription `subscriptionId` (as
|
|
273
|
+
// `getCdcSubscriptionStatus`/`replayCdcSubscription` do); the CRUD-generated
|
|
274
|
+
// stdio tool takes it as `id`, like every other `delete_*` tool.
|
|
275
|
+
// Known divergence, not a skipped gate: the harness builds `SubscriptionService`
|
|
276
|
+
// without `cache`/`audit`, so REST's `cdc_subscription_deleted` audit-log row
|
|
277
|
+
// and feed-flag cache eviction do not happen on this path. The run, tool-call
|
|
278
|
+
// and approval rows record the deletion instead, and the flag cache expires on
|
|
279
|
+
// its own TTL.
|
|
280
|
+
delete_cdc_subscription: { skill: "deleteCdcSubscription", rename: { id: "subscriptionId" } }
|
|
281
|
+
};
|
|
282
|
+
var UNGOVERNED_MUTATIONS = {
|
|
283
|
+
// Skill exists, but the canonical contract is narrower than what this tool
|
|
284
|
+
// advertises. Routing now would reject arguments callers legitimately send.
|
|
285
|
+
create_collection: "contract-narrower-than-tool: 16 advertised properties have no canonical counterpart",
|
|
286
|
+
create_policy: "contract-narrower-than-tool: enforceTfa/ipAllow/ipDeny/validFrom/validUntil",
|
|
287
|
+
create_role: "contract-narrower-than-tool: systemKey",
|
|
288
|
+
cdc_subscription_replay: "contract-narrower-than-tool: cursor has no canonical counterpart",
|
|
289
|
+
create_cdc_subscription: "contract-narrower-than-tool: payload_mode \u2014 the createCdcSubscription handler never forwards it, so a snapshot subscription would be created as reference",
|
|
290
|
+
// Skill and canonical contract both exist and accept the advertised arguments,
|
|
291
|
+
// but the skill's handler skips checks the REST route applies. Routing would
|
|
292
|
+
// add HITL and remove those checks, so these stay on REST until the handler
|
|
293
|
+
// carries them.
|
|
294
|
+
create_flow: "skill-weaker-than-rest: createFlow skips the active-graph validation and schedule-cron check of POST /flows and never sets nextRunAt, so an active schedule flow would never fire",
|
|
295
|
+
install_extension: "skill-weaker-than-rest: installExtension skips the bundle signature check, the reserved lumibase-* namespace check and the per-action extensions:* permission probes of POST /extensions",
|
|
296
|
+
update_extension: "skill-weaker-than-rest: updateExtension skips the per-action extensions:* permission probes, the unverified-official enable refusal, sandbox cache eviction and CDC subscription sync of PATCH /extensions/:id",
|
|
297
|
+
// No skill at all: nothing to route to. Listed so the set is closed.
|
|
298
|
+
add_policy_permission: "no-skill",
|
|
299
|
+
apply_access_import: "no-skill",
|
|
300
|
+
apply_schema: "no-skill",
|
|
301
|
+
approve_content: "no-skill",
|
|
302
|
+
assign_role_user: "no-skill",
|
|
303
|
+
attach_api_key_policy: "no-skill",
|
|
304
|
+
attach_api_key_role: "no-skill",
|
|
305
|
+
attach_policy_user: "no-skill",
|
|
306
|
+
attach_role_policy: "no-skill",
|
|
307
|
+
compile_intent: "no-skill",
|
|
308
|
+
create_preset: "no-skill",
|
|
309
|
+
create_release: "no-skill",
|
|
310
|
+
create_share: "no-skill",
|
|
311
|
+
delete_media: "no-skill",
|
|
312
|
+
delete_policy_permission: "no-skill",
|
|
313
|
+
delete_preset: "no-skill",
|
|
314
|
+
delete_release: "no-skill",
|
|
315
|
+
delete_tm: "no-skill",
|
|
316
|
+
detach_api_key_policy: "no-skill",
|
|
317
|
+
detach_api_key_role: "no-skill",
|
|
318
|
+
detach_policy_user: "no-skill",
|
|
319
|
+
detach_role_policy: "no-skill",
|
|
320
|
+
drop_materialization: "no-skill",
|
|
321
|
+
install_marketplace_extension: "no-skill",
|
|
322
|
+
publish_extension: "no-skill",
|
|
323
|
+
publish_release: "no-skill",
|
|
324
|
+
refresh_materialization: "no-skill",
|
|
325
|
+
reject_content: "no-skill",
|
|
326
|
+
remove_role_user: "no-skill",
|
|
327
|
+
restore_backup: "no-skill",
|
|
328
|
+
revoke_share: "no-skill",
|
|
329
|
+
run_panel: "no-skill",
|
|
330
|
+
submit_review: "no-skill",
|
|
331
|
+
translate_text: "no-skill",
|
|
332
|
+
update_cdc_subscription: "no-skill",
|
|
333
|
+
update_collection: "no-skill",
|
|
334
|
+
update_flow: "no-skill",
|
|
335
|
+
update_intent: "no-skill",
|
|
336
|
+
update_policy: "no-skill",
|
|
337
|
+
update_policy_permission: "no-skill",
|
|
338
|
+
update_preset: "no-skill",
|
|
339
|
+
update_release: "no-skill",
|
|
340
|
+
update_role: "no-skill",
|
|
341
|
+
update_team: "no-skill",
|
|
342
|
+
update_tm: "no-skill",
|
|
343
|
+
upsert_field: "no-skill",
|
|
344
|
+
upsert_tm: "no-skill"
|
|
345
|
+
};
|
|
346
|
+
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)/;
|
|
347
|
+
var MUTATION_EXCEPTIONS = /* @__PURE__ */ new Set(["cdc_subscription_replay"]);
|
|
348
|
+
function isMutationTool(name) {
|
|
349
|
+
return MUTATION_EXCEPTIONS.has(name) || MUTATION_VERB.test(name);
|
|
350
|
+
}
|
|
351
|
+
var PROMPT_ONLY_ARGS = /* @__PURE__ */ new Set(["confirm"]);
|
|
352
|
+
var camel = (key) => key.replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
353
|
+
function toSkillArgs(args, binding) {
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const [key, value] of Object.entries(args)) {
|
|
356
|
+
if (PROMPT_ONLY_ARGS.has(key)) continue;
|
|
357
|
+
if (value === void 0) continue;
|
|
358
|
+
out[binding.rename?.[key] ?? camel(key)] = value;
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function governedModeFromEnv(env = process.env) {
|
|
363
|
+
const raw = (env["LUMIBASE_MCP_GOVERNED"] ?? "auto").toLowerCase();
|
|
364
|
+
if (raw === "true" || raw === "on" || raw === "1") return "on";
|
|
365
|
+
if (raw === "false" || raw === "off" || raw === "0") return "off";
|
|
366
|
+
return "auto";
|
|
367
|
+
}
|
|
368
|
+
var GovernedDispatcher = class {
|
|
369
|
+
constructor(client, options = {}) {
|
|
370
|
+
this.client = client;
|
|
371
|
+
this.mode = options.mode ?? governedModeFromEnv();
|
|
372
|
+
this.warn = options.warn ?? ((message) => console.error(message));
|
|
373
|
+
}
|
|
374
|
+
client;
|
|
375
|
+
mode;
|
|
376
|
+
warn;
|
|
377
|
+
available;
|
|
378
|
+
get enabled() {
|
|
379
|
+
return this.mode !== "off";
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* True when this deployment requires every mutation to go through governance.
|
|
383
|
+
*
|
|
384
|
+
* Read by the registration wrapper to refuse mutations that have no governed
|
|
385
|
+
* mapping. Without it, mode `on` only governed the 27 mapped tools and left the
|
|
386
|
+
* rest on REST — so the setting that exists to guarantee governance did not,
|
|
387
|
+
* and the guarantee failed silently for exactly the calls nobody had mapped
|
|
388
|
+
* yet.
|
|
389
|
+
*/
|
|
390
|
+
get requiresGovernance() {
|
|
391
|
+
return this.mode === "on";
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The refusal returned for a mutation that cannot be governed.
|
|
395
|
+
*
|
|
396
|
+
* Names the reason from {@link UNGOVERNED_MUTATIONS} when there is one, so the
|
|
397
|
+
* operator can tell "we know about this gap" from "nobody classified this tool".
|
|
398
|
+
*/
|
|
399
|
+
refuseUngoverned(tool) {
|
|
400
|
+
const reason = UNGOVERNED_MUTATIONS[tool];
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: "text",
|
|
405
|
+
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).
|
|
406
|
+
` + (reason ? `Known gap: ${reason}.
|
|
407
|
+
` : "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."
|
|
408
|
+
}
|
|
409
|
+
],
|
|
410
|
+
isError: true
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** True when the governed endpoint answered a probe. */
|
|
414
|
+
probe() {
|
|
415
|
+
this.available ??= (async () => {
|
|
416
|
+
try {
|
|
417
|
+
await this.client.jsonRpc("tools/list");
|
|
418
|
+
return true;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (this.mode === "on") return false;
|
|
421
|
+
this.warn(
|
|
422
|
+
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.`
|
|
423
|
+
);
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
return this.available;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Runs `tool` through the harness.
|
|
431
|
+
*
|
|
432
|
+
* @returns the rendered tool result, or `undefined` when the caller should run
|
|
433
|
+
* its own REST handler instead (mode `auto` with governance unavailable).
|
|
434
|
+
*/
|
|
435
|
+
async dispatch(tool, args, binding) {
|
|
436
|
+
if (!this.enabled) return void 0;
|
|
437
|
+
if (!await this.probe()) {
|
|
438
|
+
if (this.mode === "on") {
|
|
439
|
+
return {
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: "text",
|
|
443
|
+
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.`
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
isError: true
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const result = await this.client.jsonRpc("tools/call", { name: binding.skill, arguments: toSkillArgs(args, binding) });
|
|
453
|
+
const decision = asGovernedDecision(result?.structuredContent);
|
|
454
|
+
if (decision) return renderDecision(decision, `${tool} executed.`);
|
|
455
|
+
return {
|
|
456
|
+
content: result?.content ?? [{ type: "text", text: JSON.stringify(result ?? null, null, 2) }],
|
|
457
|
+
...result?.isError === void 0 ? {} : { isError: result.isError }
|
|
458
|
+
};
|
|
459
|
+
} catch (err) {
|
|
460
|
+
return fail(err);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// src/tools/access.ts
|
|
466
|
+
var import_zod3 = require("zod");
|
|
467
|
+
|
|
468
|
+
// src/tools/_crud.ts
|
|
469
|
+
var import_zod2 = require("zod");
|
|
157
470
|
|
|
158
471
|
// src/tools/path.ts
|
|
159
472
|
var import_zod = require("zod");
|
|
@@ -240,8 +553,8 @@ function registerCrud(server, client, opts) {
|
|
|
240
553
|
async (args) => {
|
|
241
554
|
const id = String(args[idParam]);
|
|
242
555
|
return run(async () => {
|
|
243
|
-
await client.delete(`${basePath}/${encodePathSegment(id)}`);
|
|
244
|
-
return
|
|
556
|
+
const response = await client.delete(`${basePath}/${encodePathSegment(id)}`);
|
|
557
|
+
return okAfter(response, `${resource} "${id}" deleted.`);
|
|
245
558
|
});
|
|
246
559
|
}
|
|
247
560
|
);
|
|
@@ -313,8 +626,8 @@ function registerAccessTools(server, client) {
|
|
|
313
626
|
}
|
|
314
627
|
},
|
|
315
628
|
async ({ id, policyId }) => run(async () => {
|
|
316
|
-
await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
317
|
-
return
|
|
629
|
+
const response = await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
630
|
+
return okAfter(response, `Policy "${policyId}" detached from role "${id}".`);
|
|
318
631
|
})
|
|
319
632
|
);
|
|
320
633
|
server.registerTool(
|
|
@@ -336,8 +649,8 @@ function registerAccessTools(server, client) {
|
|
|
336
649
|
}
|
|
337
650
|
},
|
|
338
651
|
async ({ id, userId }) => run(async () => {
|
|
339
|
-
await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
340
|
-
return
|
|
652
|
+
const response = await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
653
|
+
return okAfter(response, `User "${userId}" removed from role "${id}".`);
|
|
341
654
|
})
|
|
342
655
|
);
|
|
343
656
|
registerCrud(server, client, {
|
|
@@ -379,8 +692,8 @@ function registerAccessTools(server, client) {
|
|
|
379
692
|
}
|
|
380
693
|
},
|
|
381
694
|
async ({ id, permId }) => run(async () => {
|
|
382
|
-
await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
|
|
383
|
-
return
|
|
695
|
+
const response = await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
|
|
696
|
+
return okAfter(response, `Permission "${permId}" deleted from policy "${id}".`);
|
|
384
697
|
})
|
|
385
698
|
);
|
|
386
699
|
server.registerTool(
|
|
@@ -407,8 +720,8 @@ function registerAccessTools(server, client) {
|
|
|
407
720
|
}
|
|
408
721
|
},
|
|
409
722
|
async ({ id, userId }) => run(async () => {
|
|
410
|
-
await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
411
|
-
return
|
|
723
|
+
const response = await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
724
|
+
return okAfter(response, `Policy "${id}" detached from user "${userId}".`);
|
|
412
725
|
})
|
|
413
726
|
);
|
|
414
727
|
server.registerTool(
|
|
@@ -516,8 +829,8 @@ function registerAdminTools(server, client) {
|
|
|
516
829
|
inputSchema: { id: idPathSegmentSchema, confirm: import_zod4.z.literal(true).describe(confirmDescription) }
|
|
517
830
|
},
|
|
518
831
|
async ({ id }) => run(async () => {
|
|
519
|
-
await client.delete(`/materialize/${encodePathSegment(id)}`);
|
|
520
|
-
return
|
|
832
|
+
const response = await client.delete(`/materialize/${encodePathSegment(id)}`);
|
|
833
|
+
return okAfter(response, `Materialization "${id}" dropped.`);
|
|
521
834
|
})
|
|
522
835
|
);
|
|
523
836
|
}
|
|
@@ -693,8 +1006,8 @@ function registerApiKeyTools(server, client) {
|
|
|
693
1006
|
}
|
|
694
1007
|
},
|
|
695
1008
|
async ({ id, roleId }) => run(async () => {
|
|
696
|
-
await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
|
|
697
|
-
return
|
|
1009
|
+
const response = await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
|
|
1010
|
+
return okAfter(response, `Role "${roleId}" detached from API key "${id}".`);
|
|
698
1011
|
})
|
|
699
1012
|
);
|
|
700
1013
|
server.registerTool(
|
|
@@ -721,8 +1034,8 @@ function registerApiKeyTools(server, client) {
|
|
|
721
1034
|
}
|
|
722
1035
|
},
|
|
723
1036
|
async ({ id, policyId }) => run(async () => {
|
|
724
|
-
await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
725
|
-
return
|
|
1037
|
+
const response = await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
1038
|
+
return okAfter(response, `Policy "${policyId}" detached from API key "${id}".`);
|
|
726
1039
|
})
|
|
727
1040
|
);
|
|
728
1041
|
}
|
|
@@ -914,8 +1227,8 @@ function registerCollectionTools(server, client) {
|
|
|
914
1227
|
},
|
|
915
1228
|
async ({ name, confirm: _ }) => {
|
|
916
1229
|
try {
|
|
917
|
-
await client.delete(`/collections/${encodePathSegment(name)}`);
|
|
918
|
-
return
|
|
1230
|
+
const response = await client.delete(`/collections/${encodePathSegment(name)}`);
|
|
1231
|
+
return okAfter(response, `Collection "${name}" deleted.`);
|
|
919
1232
|
} catch (err) {
|
|
920
1233
|
return { content: [{ type: "text", text: `Error: ${formatError2(err)}` }], isError: true };
|
|
921
1234
|
}
|
|
@@ -1071,8 +1384,8 @@ function registerContentConfigTools(server, client) {
|
|
|
1071
1384
|
}
|
|
1072
1385
|
},
|
|
1073
1386
|
async ({ key }) => run(async () => {
|
|
1074
|
-
await client.delete(`/settings/${encodePathSegment(key)}`);
|
|
1075
|
-
return
|
|
1387
|
+
const response = await client.delete(`/settings/${encodePathSegment(key)}`);
|
|
1388
|
+
return okAfter(response, `Setting "${key}" deleted.`);
|
|
1076
1389
|
})
|
|
1077
1390
|
);
|
|
1078
1391
|
}
|
|
@@ -1235,8 +1548,8 @@ function registerExtensionTools(server, client) {
|
|
|
1235
1548
|
inputSchema: { id: idPathSegmentSchema, confirm: import_zod12.z.literal(true).describe(confirmDescription) }
|
|
1236
1549
|
},
|
|
1237
1550
|
async ({ id }) => run(async () => {
|
|
1238
|
-
await client.delete(`/extensions/${encodePathSegment(id)}`);
|
|
1239
|
-
return
|
|
1551
|
+
const response = await client.delete(`/extensions/${encodePathSegment(id)}`);
|
|
1552
|
+
return okAfter(response, `Extension "${id}" uninstalled.`);
|
|
1240
1553
|
})
|
|
1241
1554
|
);
|
|
1242
1555
|
server.registerTool(
|
|
@@ -1392,12 +1705,10 @@ function registerFieldTools(server, client) {
|
|
|
1392
1705
|
async ({ collection, field_name, force }) => {
|
|
1393
1706
|
try {
|
|
1394
1707
|
const qs = force ? "?force=true" : "";
|
|
1395
|
-
await client.delete(
|
|
1708
|
+
const response = await client.delete(
|
|
1396
1709
|
`/collections/${encodePathSegment(collection)}/fields/${encodePathSegment(field_name)}${qs}`
|
|
1397
1710
|
);
|
|
1398
|
-
return {
|
|
1399
|
-
content: [{ type: "text", text: `Field "${field_name}" deleted from "${collection}".` }]
|
|
1400
|
-
};
|
|
1711
|
+
return okAfter(response, `Field "${field_name}" deleted from "${collection}".`);
|
|
1401
1712
|
} catch (err) {
|
|
1402
1713
|
return { content: [{ type: "text", text: `Error: ${formatError3(err)}` }], isError: true };
|
|
1403
1714
|
}
|
|
@@ -1564,7 +1875,7 @@ function registerItemTools(server, client) {
|
|
|
1564
1875
|
async ({ collection, data: itemData, status }) => {
|
|
1565
1876
|
try {
|
|
1566
1877
|
const data = await client.post(`/items/${encodePathSegment(collection)}`, {
|
|
1567
|
-
|
|
1878
|
+
data: itemData,
|
|
1568
1879
|
status
|
|
1569
1880
|
});
|
|
1570
1881
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -1587,7 +1898,7 @@ function registerItemTools(server, client) {
|
|
|
1587
1898
|
try {
|
|
1588
1899
|
const data = await client.patch(
|
|
1589
1900
|
`/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`,
|
|
1590
|
-
itemData
|
|
1901
|
+
{ data: itemData }
|
|
1591
1902
|
);
|
|
1592
1903
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1593
1904
|
} catch (err) {
|
|
@@ -1607,8 +1918,10 @@ function registerItemTools(server, client) {
|
|
|
1607
1918
|
},
|
|
1608
1919
|
async ({ collection, id }) => {
|
|
1609
1920
|
try {
|
|
1610
|
-
await client.delete(
|
|
1611
|
-
|
|
1921
|
+
const response = await client.delete(
|
|
1922
|
+
`/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`
|
|
1923
|
+
);
|
|
1924
|
+
return okAfter(response, `Item "${id}" deleted from "${collection}".`);
|
|
1612
1925
|
} catch (err) {
|
|
1613
1926
|
return { content: [{ type: "text", text: `Error: ${formatError4(err)}` }], isError: true };
|
|
1614
1927
|
}
|
|
@@ -1696,8 +2009,8 @@ function registerReleaseTools(server, client) {
|
|
|
1696
2009
|
}
|
|
1697
2010
|
},
|
|
1698
2011
|
async ({ id }) => run(async () => {
|
|
1699
|
-
await client.delete(`/releases/${encodePathSegment(id)}`);
|
|
1700
|
-
return
|
|
2012
|
+
const response = await client.delete(`/releases/${encodePathSegment(id)}`);
|
|
2013
|
+
return okAfter(response, `Release "${id}" deleted.`);
|
|
1701
2014
|
})
|
|
1702
2015
|
);
|
|
1703
2016
|
}
|
|
@@ -1731,8 +2044,8 @@ function registerShareTools(server, client) {
|
|
|
1731
2044
|
}
|
|
1732
2045
|
},
|
|
1733
2046
|
async ({ id }) => run(async () => {
|
|
1734
|
-
await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
|
|
1735
|
-
return
|
|
2047
|
+
const response = await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
|
|
2048
|
+
return okAfter(response, `Share link "${id}" revoked.`);
|
|
1736
2049
|
})
|
|
1737
2050
|
);
|
|
1738
2051
|
}
|
|
@@ -1841,8 +2154,8 @@ function registerRelationTools(server, client) {
|
|
|
1841
2154
|
}
|
|
1842
2155
|
},
|
|
1843
2156
|
async ({ id }) => run(async () => {
|
|
1844
|
-
await client.delete(`/relations/${encodePathSegment(id)}`);
|
|
1845
|
-
return
|
|
2157
|
+
const response = await client.delete(`/relations/${encodePathSegment(id)}`);
|
|
2158
|
+
return okAfter(response, `Relation "${id}" deleted.`);
|
|
1846
2159
|
})
|
|
1847
2160
|
);
|
|
1848
2161
|
}
|
|
@@ -1887,8 +2200,8 @@ function registerSearchMediaTools(server, client) {
|
|
|
1887
2200
|
}
|
|
1888
2201
|
},
|
|
1889
2202
|
async ({ key }) => run(async () => {
|
|
1890
|
-
await client.delete(`/media/${encodeMediaKey(key)}`);
|
|
1891
|
-
return
|
|
2203
|
+
const response = await client.delete(`/media/${encodeMediaKey(key)}`);
|
|
2204
|
+
return okAfter(response, `Media asset "${key}" deleted.`);
|
|
1892
2205
|
})
|
|
1893
2206
|
);
|
|
1894
2207
|
server.registerTool(
|
|
@@ -1984,8 +2297,8 @@ function registerTranslationMemoryTools(server, client) {
|
|
|
1984
2297
|
}
|
|
1985
2298
|
},
|
|
1986
2299
|
async ({ id }) => run(async () => {
|
|
1987
|
-
await client.delete(`/tm/${encodePathSegment(id)}`);
|
|
1988
|
-
return
|
|
2300
|
+
const response = await client.delete(`/tm/${encodePathSegment(id)}`);
|
|
2301
|
+
return okAfter(response, `Translation-memory entry "${id}" deleted.`);
|
|
1989
2302
|
})
|
|
1990
2303
|
);
|
|
1991
2304
|
}
|
|
@@ -2033,8 +2346,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2033
2346
|
inputSchema: { id: idPathSegmentSchema, confirm: import_zod23.z.literal(true).describe(confirmDescription) }
|
|
2034
2347
|
},
|
|
2035
2348
|
async ({ id }) => run(async () => {
|
|
2036
|
-
await client.delete(`/users/${encodePathSegment(id)}`);
|
|
2037
|
-
return
|
|
2349
|
+
const response = await client.delete(`/users/${encodePathSegment(id)}`);
|
|
2350
|
+
return okAfter(response, `User "${id}" removed from the site.`);
|
|
2038
2351
|
})
|
|
2039
2352
|
);
|
|
2040
2353
|
server.registerTool(
|
|
@@ -2074,8 +2387,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2074
2387
|
inputSchema: { id: idPathSegmentSchema, confirm: import_zod23.z.literal(true).describe(confirmDescription) }
|
|
2075
2388
|
},
|
|
2076
2389
|
async ({ id }) => run(async () => {
|
|
2077
|
-
await client.delete(`/teams/${encodePathSegment(id)}`);
|
|
2078
|
-
return
|
|
2390
|
+
const response = await client.delete(`/teams/${encodePathSegment(id)}`);
|
|
2391
|
+
return okAfter(response, `Team "${id}" deleted.`);
|
|
2079
2392
|
})
|
|
2080
2393
|
);
|
|
2081
2394
|
server.registerTool(
|
|
@@ -2102,8 +2415,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2102
2415
|
}
|
|
2103
2416
|
},
|
|
2104
2417
|
async ({ id, userId }) => run(async () => {
|
|
2105
|
-
await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
|
|
2106
|
-
return
|
|
2418
|
+
const response = await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
|
|
2419
|
+
return okAfter(response, `User "${userId}" removed from team "${id}".`);
|
|
2107
2420
|
})
|
|
2108
2421
|
);
|
|
2109
2422
|
}
|
|
@@ -2132,7 +2445,33 @@ function registerWebhookTools(server, client) {
|
|
|
2132
2445
|
}
|
|
2133
2446
|
|
|
2134
2447
|
// src/tools/index.ts
|
|
2135
|
-
function registerAllTools(server, client) {
|
|
2448
|
+
function registerAllTools(server, client, options = {}) {
|
|
2449
|
+
const dispatcher = options.dispatcher === void 0 ? new GovernedDispatcher(client) : options.dispatcher;
|
|
2450
|
+
const target = dispatcher?.enabled ? withGovernedHandlers(server, dispatcher) : server;
|
|
2451
|
+
registerModules(target, client);
|
|
2452
|
+
}
|
|
2453
|
+
function withGovernedHandlers(server, dispatcher) {
|
|
2454
|
+
return new Proxy(server, {
|
|
2455
|
+
get(t, prop, receiver) {
|
|
2456
|
+
if (prop !== "registerTool") return Reflect.get(t, prop, receiver);
|
|
2457
|
+
return (name, config, handler) => {
|
|
2458
|
+
const binding = GOVERNED_TOOLS[name];
|
|
2459
|
+
let wrapped = handler;
|
|
2460
|
+
if (binding) {
|
|
2461
|
+
wrapped = async (args) => await dispatcher.dispatch(name, args, binding) ?? await handler(args);
|
|
2462
|
+
} else if (dispatcher.requiresGovernance && isMutationTool(name)) {
|
|
2463
|
+
wrapped = async () => dispatcher.refuseUngoverned(name);
|
|
2464
|
+
}
|
|
2465
|
+
return t.registerTool(
|
|
2466
|
+
name,
|
|
2467
|
+
config,
|
|
2468
|
+
wrapped
|
|
2469
|
+
);
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
function registerModules(server, client) {
|
|
2136
2475
|
registerCollectionTools(server, client);
|
|
2137
2476
|
registerFieldTools(server, client);
|
|
2138
2477
|
registerItemTools(server, client);
|
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,285 @@ 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
|
+
create_relation: { skill: "createRelation" },
|
|
237
|
+
delete_relation: { skill: "deleteRelation" },
|
|
238
|
+
// ── access ───────────────────────────────────────────────────────────────
|
|
239
|
+
delete_role: { skill: "deleteRole" },
|
|
240
|
+
delete_policy: { skill: "deletePolicy" },
|
|
241
|
+
// ── automation ───────────────────────────────────────────────────────────
|
|
242
|
+
delete_flow: { skill: "deleteFlow" },
|
|
243
|
+
run_flow: { skill: "runFlow" },
|
|
244
|
+
create_intent: { skill: "createIntent" },
|
|
245
|
+
delete_intent: { skill: "deleteIntent" },
|
|
246
|
+
// ── config ───────────────────────────────────────────────────────────────
|
|
247
|
+
upsert_setting: { skill: "upsertSetting" },
|
|
248
|
+
delete_setting: { skill: "deleteSetting" },
|
|
249
|
+
create_translation: { skill: "createTranslation" },
|
|
250
|
+
update_translation: { skill: "updateTranslation" },
|
|
251
|
+
delete_translation: { skill: "deleteTranslation" },
|
|
252
|
+
create_webhook: { skill: "createWebhook" },
|
|
253
|
+
update_webhook: { skill: "updateWebhook" },
|
|
254
|
+
delete_webhook: { skill: "deleteWebhook" },
|
|
255
|
+
// ── api keys / users / teams ──────────────────────────────────────────────
|
|
256
|
+
create_api_key: { skill: "createApiKey" },
|
|
257
|
+
rotate_api_key: { skill: "rotateApiKey" },
|
|
258
|
+
revoke_api_key: { skill: "revokeApiKey" },
|
|
259
|
+
invite_user: { skill: "inviteUser" },
|
|
260
|
+
update_user: { skill: "updateUser" },
|
|
261
|
+
remove_user: { skill: "removeUser" },
|
|
262
|
+
create_team: { skill: "createTeam" },
|
|
263
|
+
delete_team: { skill: "deleteTeam" },
|
|
264
|
+
// The team tools take the team as `id`; the skill names it `teamId` because it
|
|
265
|
+
// also takes a `userId` and one bare `id` would be ambiguous.
|
|
266
|
+
add_team_member: { skill: "addTeamMember", rename: { id: "teamId" } },
|
|
267
|
+
remove_team_member: { skill: "removeTeamMember", rename: { id: "teamId" } },
|
|
268
|
+
// ── extensions ───────────────────────────────────────────────────────────
|
|
269
|
+
uninstall_extension: { skill: "uninstallExtension" },
|
|
270
|
+
// ── cdc ──────────────────────────────────────────────────────────────────
|
|
271
|
+
// The change-feed skills name the subscription `subscriptionId` (as
|
|
272
|
+
// `getCdcSubscriptionStatus`/`replayCdcSubscription` do); the CRUD-generated
|
|
273
|
+
// stdio tool takes it as `id`, like every other `delete_*` tool.
|
|
274
|
+
// Known divergence, not a skipped gate: the harness builds `SubscriptionService`
|
|
275
|
+
// without `cache`/`audit`, so REST's `cdc_subscription_deleted` audit-log row
|
|
276
|
+
// and feed-flag cache eviction do not happen on this path. The run, tool-call
|
|
277
|
+
// and approval rows record the deletion instead, and the flag cache expires on
|
|
278
|
+
// its own TTL.
|
|
279
|
+
delete_cdc_subscription: { skill: "deleteCdcSubscription", rename: { id: "subscriptionId" } }
|
|
280
|
+
};
|
|
281
|
+
var UNGOVERNED_MUTATIONS = {
|
|
282
|
+
// Skill exists, but the canonical contract is narrower than what this tool
|
|
283
|
+
// advertises. Routing now would reject arguments callers legitimately send.
|
|
284
|
+
create_collection: "contract-narrower-than-tool: 16 advertised properties have no canonical counterpart",
|
|
285
|
+
create_policy: "contract-narrower-than-tool: enforceTfa/ipAllow/ipDeny/validFrom/validUntil",
|
|
286
|
+
create_role: "contract-narrower-than-tool: systemKey",
|
|
287
|
+
cdc_subscription_replay: "contract-narrower-than-tool: cursor has no canonical counterpart",
|
|
288
|
+
create_cdc_subscription: "contract-narrower-than-tool: payload_mode \u2014 the createCdcSubscription handler never forwards it, so a snapshot subscription would be created as reference",
|
|
289
|
+
// Skill and canonical contract both exist and accept the advertised arguments,
|
|
290
|
+
// but the skill's handler skips checks the REST route applies. Routing would
|
|
291
|
+
// add HITL and remove those checks, so these stay on REST until the handler
|
|
292
|
+
// carries them.
|
|
293
|
+
create_flow: "skill-weaker-than-rest: createFlow skips the active-graph validation and schedule-cron check of POST /flows and never sets nextRunAt, so an active schedule flow would never fire",
|
|
294
|
+
install_extension: "skill-weaker-than-rest: installExtension skips the bundle signature check, the reserved lumibase-* namespace check and the per-action extensions:* permission probes of POST /extensions",
|
|
295
|
+
update_extension: "skill-weaker-than-rest: updateExtension skips the per-action extensions:* permission probes, the unverified-official enable refusal, sandbox cache eviction and CDC subscription sync of PATCH /extensions/:id",
|
|
296
|
+
// No skill at all: nothing to route to. Listed so the set is closed.
|
|
297
|
+
add_policy_permission: "no-skill",
|
|
298
|
+
apply_access_import: "no-skill",
|
|
299
|
+
apply_schema: "no-skill",
|
|
300
|
+
approve_content: "no-skill",
|
|
301
|
+
assign_role_user: "no-skill",
|
|
302
|
+
attach_api_key_policy: "no-skill",
|
|
303
|
+
attach_api_key_role: "no-skill",
|
|
304
|
+
attach_policy_user: "no-skill",
|
|
305
|
+
attach_role_policy: "no-skill",
|
|
306
|
+
compile_intent: "no-skill",
|
|
307
|
+
create_preset: "no-skill",
|
|
308
|
+
create_release: "no-skill",
|
|
309
|
+
create_share: "no-skill",
|
|
310
|
+
delete_media: "no-skill",
|
|
311
|
+
delete_policy_permission: "no-skill",
|
|
312
|
+
delete_preset: "no-skill",
|
|
313
|
+
delete_release: "no-skill",
|
|
314
|
+
delete_tm: "no-skill",
|
|
315
|
+
detach_api_key_policy: "no-skill",
|
|
316
|
+
detach_api_key_role: "no-skill",
|
|
317
|
+
detach_policy_user: "no-skill",
|
|
318
|
+
detach_role_policy: "no-skill",
|
|
319
|
+
drop_materialization: "no-skill",
|
|
320
|
+
install_marketplace_extension: "no-skill",
|
|
321
|
+
publish_extension: "no-skill",
|
|
322
|
+
publish_release: "no-skill",
|
|
323
|
+
refresh_materialization: "no-skill",
|
|
324
|
+
reject_content: "no-skill",
|
|
325
|
+
remove_role_user: "no-skill",
|
|
326
|
+
restore_backup: "no-skill",
|
|
327
|
+
revoke_share: "no-skill",
|
|
328
|
+
run_panel: "no-skill",
|
|
329
|
+
submit_review: "no-skill",
|
|
330
|
+
translate_text: "no-skill",
|
|
331
|
+
update_cdc_subscription: "no-skill",
|
|
332
|
+
update_collection: "no-skill",
|
|
333
|
+
update_flow: "no-skill",
|
|
334
|
+
update_intent: "no-skill",
|
|
335
|
+
update_policy: "no-skill",
|
|
336
|
+
update_policy_permission: "no-skill",
|
|
337
|
+
update_preset: "no-skill",
|
|
338
|
+
update_release: "no-skill",
|
|
339
|
+
update_role: "no-skill",
|
|
340
|
+
update_team: "no-skill",
|
|
341
|
+
update_tm: "no-skill",
|
|
342
|
+
upsert_field: "no-skill",
|
|
343
|
+
upsert_tm: "no-skill"
|
|
344
|
+
};
|
|
345
|
+
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)/;
|
|
346
|
+
var MUTATION_EXCEPTIONS = /* @__PURE__ */ new Set(["cdc_subscription_replay"]);
|
|
347
|
+
function isMutationTool(name) {
|
|
348
|
+
return MUTATION_EXCEPTIONS.has(name) || MUTATION_VERB.test(name);
|
|
349
|
+
}
|
|
350
|
+
var PROMPT_ONLY_ARGS = /* @__PURE__ */ new Set(["confirm"]);
|
|
351
|
+
var camel = (key) => key.replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
352
|
+
function toSkillArgs(args, binding) {
|
|
353
|
+
const out = {};
|
|
354
|
+
for (const [key, value] of Object.entries(args)) {
|
|
355
|
+
if (PROMPT_ONLY_ARGS.has(key)) continue;
|
|
356
|
+
if (value === void 0) continue;
|
|
357
|
+
out[binding.rename?.[key] ?? camel(key)] = value;
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function governedModeFromEnv(env = process.env) {
|
|
362
|
+
const raw = (env["LUMIBASE_MCP_GOVERNED"] ?? "auto").toLowerCase();
|
|
363
|
+
if (raw === "true" || raw === "on" || raw === "1") return "on";
|
|
364
|
+
if (raw === "false" || raw === "off" || raw === "0") return "off";
|
|
365
|
+
return "auto";
|
|
366
|
+
}
|
|
367
|
+
var GovernedDispatcher = class {
|
|
368
|
+
constructor(client, options = {}) {
|
|
369
|
+
this.client = client;
|
|
370
|
+
this.mode = options.mode ?? governedModeFromEnv();
|
|
371
|
+
this.warn = options.warn ?? ((message) => console.error(message));
|
|
372
|
+
}
|
|
373
|
+
client;
|
|
374
|
+
mode;
|
|
375
|
+
warn;
|
|
376
|
+
available;
|
|
377
|
+
get enabled() {
|
|
378
|
+
return this.mode !== "off";
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* True when this deployment requires every mutation to go through governance.
|
|
382
|
+
*
|
|
383
|
+
* Read by the registration wrapper to refuse mutations that have no governed
|
|
384
|
+
* mapping. Without it, mode `on` only governed the 27 mapped tools and left the
|
|
385
|
+
* rest on REST — so the setting that exists to guarantee governance did not,
|
|
386
|
+
* and the guarantee failed silently for exactly the calls nobody had mapped
|
|
387
|
+
* yet.
|
|
388
|
+
*/
|
|
389
|
+
get requiresGovernance() {
|
|
390
|
+
return this.mode === "on";
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The refusal returned for a mutation that cannot be governed.
|
|
394
|
+
*
|
|
395
|
+
* Names the reason from {@link UNGOVERNED_MUTATIONS} when there is one, so the
|
|
396
|
+
* operator can tell "we know about this gap" from "nobody classified this tool".
|
|
397
|
+
*/
|
|
398
|
+
refuseUngoverned(tool) {
|
|
399
|
+
const reason = UNGOVERNED_MUTATIONS[tool];
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: "text",
|
|
404
|
+
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).
|
|
405
|
+
` + (reason ? `Known gap: ${reason}.
|
|
406
|
+
` : "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."
|
|
407
|
+
}
|
|
408
|
+
],
|
|
409
|
+
isError: true
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** True when the governed endpoint answered a probe. */
|
|
413
|
+
probe() {
|
|
414
|
+
this.available ??= (async () => {
|
|
415
|
+
try {
|
|
416
|
+
await this.client.jsonRpc("tools/list");
|
|
417
|
+
return true;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
if (this.mode === "on") return false;
|
|
420
|
+
this.warn(
|
|
421
|
+
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.`
|
|
422
|
+
);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
426
|
+
return this.available;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Runs `tool` through the harness.
|
|
430
|
+
*
|
|
431
|
+
* @returns the rendered tool result, or `undefined` when the caller should run
|
|
432
|
+
* its own REST handler instead (mode `auto` with governance unavailable).
|
|
433
|
+
*/
|
|
434
|
+
async dispatch(tool, args, binding) {
|
|
435
|
+
if (!this.enabled) return void 0;
|
|
436
|
+
if (!await this.probe()) {
|
|
437
|
+
if (this.mode === "on") {
|
|
438
|
+
return {
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: "text",
|
|
442
|
+
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.`
|
|
443
|
+
}
|
|
444
|
+
],
|
|
445
|
+
isError: true
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return void 0;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const result = await this.client.jsonRpc("tools/call", { name: binding.skill, arguments: toSkillArgs(args, binding) });
|
|
452
|
+
const decision = asGovernedDecision(result?.structuredContent);
|
|
453
|
+
if (decision) return renderDecision(decision, `${tool} executed.`);
|
|
454
|
+
return {
|
|
455
|
+
content: result?.content ?? [{ type: "text", text: JSON.stringify(result ?? null, null, 2) }],
|
|
456
|
+
...result?.isError === void 0 ? {} : { isError: result.isError }
|
|
457
|
+
};
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return fail(err);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/tools/access.ts
|
|
465
|
+
import { z as z3 } from "zod";
|
|
466
|
+
|
|
467
|
+
// src/tools/_crud.ts
|
|
468
|
+
import { z as z2 } from "zod";
|
|
156
469
|
|
|
157
470
|
// src/tools/path.ts
|
|
158
471
|
import { z } from "zod";
|
|
@@ -239,8 +552,8 @@ function registerCrud(server, client, opts) {
|
|
|
239
552
|
async (args) => {
|
|
240
553
|
const id = String(args[idParam]);
|
|
241
554
|
return run(async () => {
|
|
242
|
-
await client.delete(`${basePath}/${encodePathSegment(id)}`);
|
|
243
|
-
return
|
|
555
|
+
const response = await client.delete(`${basePath}/${encodePathSegment(id)}`);
|
|
556
|
+
return okAfter(response, `${resource} "${id}" deleted.`);
|
|
244
557
|
});
|
|
245
558
|
}
|
|
246
559
|
);
|
|
@@ -312,8 +625,8 @@ function registerAccessTools(server, client) {
|
|
|
312
625
|
}
|
|
313
626
|
},
|
|
314
627
|
async ({ id, policyId }) => run(async () => {
|
|
315
|
-
await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
316
|
-
return
|
|
628
|
+
const response = await client.delete(`/roles/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
629
|
+
return okAfter(response, `Policy "${policyId}" detached from role "${id}".`);
|
|
317
630
|
})
|
|
318
631
|
);
|
|
319
632
|
server.registerTool(
|
|
@@ -335,8 +648,8 @@ function registerAccessTools(server, client) {
|
|
|
335
648
|
}
|
|
336
649
|
},
|
|
337
650
|
async ({ id, userId }) => run(async () => {
|
|
338
|
-
await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
339
|
-
return
|
|
651
|
+
const response = await client.delete(`/roles/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
652
|
+
return okAfter(response, `User "${userId}" removed from role "${id}".`);
|
|
340
653
|
})
|
|
341
654
|
);
|
|
342
655
|
registerCrud(server, client, {
|
|
@@ -378,8 +691,8 @@ function registerAccessTools(server, client) {
|
|
|
378
691
|
}
|
|
379
692
|
},
|
|
380
693
|
async ({ id, permId }) => run(async () => {
|
|
381
|
-
await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
|
|
382
|
-
return
|
|
694
|
+
const response = await client.delete(`/policies/${encodePathSegment(id)}/permissions/${encodePathSegment(permId)}`);
|
|
695
|
+
return okAfter(response, `Permission "${permId}" deleted from policy "${id}".`);
|
|
383
696
|
})
|
|
384
697
|
);
|
|
385
698
|
server.registerTool(
|
|
@@ -406,8 +719,8 @@ function registerAccessTools(server, client) {
|
|
|
406
719
|
}
|
|
407
720
|
},
|
|
408
721
|
async ({ id, userId }) => run(async () => {
|
|
409
|
-
await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
410
|
-
return
|
|
722
|
+
const response = await client.delete(`/policies/${encodePathSegment(id)}/users/${encodePathSegment(userId)}`);
|
|
723
|
+
return okAfter(response, `Policy "${id}" detached from user "${userId}".`);
|
|
411
724
|
})
|
|
412
725
|
);
|
|
413
726
|
server.registerTool(
|
|
@@ -515,8 +828,8 @@ function registerAdminTools(server, client) {
|
|
|
515
828
|
inputSchema: { id: idPathSegmentSchema, confirm: z4.literal(true).describe(confirmDescription) }
|
|
516
829
|
},
|
|
517
830
|
async ({ id }) => run(async () => {
|
|
518
|
-
await client.delete(`/materialize/${encodePathSegment(id)}`);
|
|
519
|
-
return
|
|
831
|
+
const response = await client.delete(`/materialize/${encodePathSegment(id)}`);
|
|
832
|
+
return okAfter(response, `Materialization "${id}" dropped.`);
|
|
520
833
|
})
|
|
521
834
|
);
|
|
522
835
|
}
|
|
@@ -692,8 +1005,8 @@ function registerApiKeyTools(server, client) {
|
|
|
692
1005
|
}
|
|
693
1006
|
},
|
|
694
1007
|
async ({ id, roleId }) => run(async () => {
|
|
695
|
-
await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
|
|
696
|
-
return
|
|
1008
|
+
const response = await client.delete(`/api-keys/${encodePathSegment(id)}/roles/${encodePathSegment(roleId)}`);
|
|
1009
|
+
return okAfter(response, `Role "${roleId}" detached from API key "${id}".`);
|
|
697
1010
|
})
|
|
698
1011
|
);
|
|
699
1012
|
server.registerTool(
|
|
@@ -720,8 +1033,8 @@ function registerApiKeyTools(server, client) {
|
|
|
720
1033
|
}
|
|
721
1034
|
},
|
|
722
1035
|
async ({ id, policyId }) => run(async () => {
|
|
723
|
-
await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
724
|
-
return
|
|
1036
|
+
const response = await client.delete(`/api-keys/${encodePathSegment(id)}/policies/${encodePathSegment(policyId)}`);
|
|
1037
|
+
return okAfter(response, `Policy "${policyId}" detached from API key "${id}".`);
|
|
725
1038
|
})
|
|
726
1039
|
);
|
|
727
1040
|
}
|
|
@@ -913,8 +1226,8 @@ function registerCollectionTools(server, client) {
|
|
|
913
1226
|
},
|
|
914
1227
|
async ({ name, confirm: _ }) => {
|
|
915
1228
|
try {
|
|
916
|
-
await client.delete(`/collections/${encodePathSegment(name)}`);
|
|
917
|
-
return
|
|
1229
|
+
const response = await client.delete(`/collections/${encodePathSegment(name)}`);
|
|
1230
|
+
return okAfter(response, `Collection "${name}" deleted.`);
|
|
918
1231
|
} catch (err) {
|
|
919
1232
|
return { content: [{ type: "text", text: `Error: ${formatError2(err)}` }], isError: true };
|
|
920
1233
|
}
|
|
@@ -1070,8 +1383,8 @@ function registerContentConfigTools(server, client) {
|
|
|
1070
1383
|
}
|
|
1071
1384
|
},
|
|
1072
1385
|
async ({ key }) => run(async () => {
|
|
1073
|
-
await client.delete(`/settings/${encodePathSegment(key)}`);
|
|
1074
|
-
return
|
|
1386
|
+
const response = await client.delete(`/settings/${encodePathSegment(key)}`);
|
|
1387
|
+
return okAfter(response, `Setting "${key}" deleted.`);
|
|
1075
1388
|
})
|
|
1076
1389
|
);
|
|
1077
1390
|
}
|
|
@@ -1234,8 +1547,8 @@ function registerExtensionTools(server, client) {
|
|
|
1234
1547
|
inputSchema: { id: idPathSegmentSchema, confirm: z12.literal(true).describe(confirmDescription) }
|
|
1235
1548
|
},
|
|
1236
1549
|
async ({ id }) => run(async () => {
|
|
1237
|
-
await client.delete(`/extensions/${encodePathSegment(id)}`);
|
|
1238
|
-
return
|
|
1550
|
+
const response = await client.delete(`/extensions/${encodePathSegment(id)}`);
|
|
1551
|
+
return okAfter(response, `Extension "${id}" uninstalled.`);
|
|
1239
1552
|
})
|
|
1240
1553
|
);
|
|
1241
1554
|
server.registerTool(
|
|
@@ -1391,12 +1704,10 @@ function registerFieldTools(server, client) {
|
|
|
1391
1704
|
async ({ collection, field_name, force }) => {
|
|
1392
1705
|
try {
|
|
1393
1706
|
const qs = force ? "?force=true" : "";
|
|
1394
|
-
await client.delete(
|
|
1707
|
+
const response = await client.delete(
|
|
1395
1708
|
`/collections/${encodePathSegment(collection)}/fields/${encodePathSegment(field_name)}${qs}`
|
|
1396
1709
|
);
|
|
1397
|
-
return {
|
|
1398
|
-
content: [{ type: "text", text: `Field "${field_name}" deleted from "${collection}".` }]
|
|
1399
|
-
};
|
|
1710
|
+
return okAfter(response, `Field "${field_name}" deleted from "${collection}".`);
|
|
1400
1711
|
} catch (err) {
|
|
1401
1712
|
return { content: [{ type: "text", text: `Error: ${formatError3(err)}` }], isError: true };
|
|
1402
1713
|
}
|
|
@@ -1563,7 +1874,7 @@ function registerItemTools(server, client) {
|
|
|
1563
1874
|
async ({ collection, data: itemData, status }) => {
|
|
1564
1875
|
try {
|
|
1565
1876
|
const data = await client.post(`/items/${encodePathSegment(collection)}`, {
|
|
1566
|
-
|
|
1877
|
+
data: itemData,
|
|
1567
1878
|
status
|
|
1568
1879
|
});
|
|
1569
1880
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -1586,7 +1897,7 @@ function registerItemTools(server, client) {
|
|
|
1586
1897
|
try {
|
|
1587
1898
|
const data = await client.patch(
|
|
1588
1899
|
`/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`,
|
|
1589
|
-
itemData
|
|
1900
|
+
{ data: itemData }
|
|
1590
1901
|
);
|
|
1591
1902
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1592
1903
|
} catch (err) {
|
|
@@ -1606,8 +1917,10 @@ function registerItemTools(server, client) {
|
|
|
1606
1917
|
},
|
|
1607
1918
|
async ({ collection, id }) => {
|
|
1608
1919
|
try {
|
|
1609
|
-
await client.delete(
|
|
1610
|
-
|
|
1920
|
+
const response = await client.delete(
|
|
1921
|
+
`/items/${encodePathSegment(collection)}/${encodePathSegment(id)}`
|
|
1922
|
+
);
|
|
1923
|
+
return okAfter(response, `Item "${id}" deleted from "${collection}".`);
|
|
1611
1924
|
} catch (err) {
|
|
1612
1925
|
return { content: [{ type: "text", text: `Error: ${formatError4(err)}` }], isError: true };
|
|
1613
1926
|
}
|
|
@@ -1695,8 +2008,8 @@ function registerReleaseTools(server, client) {
|
|
|
1695
2008
|
}
|
|
1696
2009
|
},
|
|
1697
2010
|
async ({ id }) => run(async () => {
|
|
1698
|
-
await client.delete(`/releases/${encodePathSegment(id)}`);
|
|
1699
|
-
return
|
|
2011
|
+
const response = await client.delete(`/releases/${encodePathSegment(id)}`);
|
|
2012
|
+
return okAfter(response, `Release "${id}" deleted.`);
|
|
1700
2013
|
})
|
|
1701
2014
|
);
|
|
1702
2015
|
}
|
|
@@ -1730,8 +2043,8 @@ function registerShareTools(server, client) {
|
|
|
1730
2043
|
}
|
|
1731
2044
|
},
|
|
1732
2045
|
async ({ id }) => run(async () => {
|
|
1733
|
-
await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
|
|
1734
|
-
return
|
|
2046
|
+
const response = await client.post(`/shares/${encodePathSegment(id)}/revoke`, {});
|
|
2047
|
+
return okAfter(response, `Share link "${id}" revoked.`);
|
|
1735
2048
|
})
|
|
1736
2049
|
);
|
|
1737
2050
|
}
|
|
@@ -1840,8 +2153,8 @@ function registerRelationTools(server, client) {
|
|
|
1840
2153
|
}
|
|
1841
2154
|
},
|
|
1842
2155
|
async ({ id }) => run(async () => {
|
|
1843
|
-
await client.delete(`/relations/${encodePathSegment(id)}`);
|
|
1844
|
-
return
|
|
2156
|
+
const response = await client.delete(`/relations/${encodePathSegment(id)}`);
|
|
2157
|
+
return okAfter(response, `Relation "${id}" deleted.`);
|
|
1845
2158
|
})
|
|
1846
2159
|
);
|
|
1847
2160
|
}
|
|
@@ -1886,8 +2199,8 @@ function registerSearchMediaTools(server, client) {
|
|
|
1886
2199
|
}
|
|
1887
2200
|
},
|
|
1888
2201
|
async ({ key }) => run(async () => {
|
|
1889
|
-
await client.delete(`/media/${encodeMediaKey(key)}`);
|
|
1890
|
-
return
|
|
2202
|
+
const response = await client.delete(`/media/${encodeMediaKey(key)}`);
|
|
2203
|
+
return okAfter(response, `Media asset "${key}" deleted.`);
|
|
1891
2204
|
})
|
|
1892
2205
|
);
|
|
1893
2206
|
server.registerTool(
|
|
@@ -1983,8 +2296,8 @@ function registerTranslationMemoryTools(server, client) {
|
|
|
1983
2296
|
}
|
|
1984
2297
|
},
|
|
1985
2298
|
async ({ id }) => run(async () => {
|
|
1986
|
-
await client.delete(`/tm/${encodePathSegment(id)}`);
|
|
1987
|
-
return
|
|
2299
|
+
const response = await client.delete(`/tm/${encodePathSegment(id)}`);
|
|
2300
|
+
return okAfter(response, `Translation-memory entry "${id}" deleted.`);
|
|
1988
2301
|
})
|
|
1989
2302
|
);
|
|
1990
2303
|
}
|
|
@@ -2032,8 +2345,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2032
2345
|
inputSchema: { id: idPathSegmentSchema, confirm: z23.literal(true).describe(confirmDescription) }
|
|
2033
2346
|
},
|
|
2034
2347
|
async ({ id }) => run(async () => {
|
|
2035
|
-
await client.delete(`/users/${encodePathSegment(id)}`);
|
|
2036
|
-
return
|
|
2348
|
+
const response = await client.delete(`/users/${encodePathSegment(id)}`);
|
|
2349
|
+
return okAfter(response, `User "${id}" removed from the site.`);
|
|
2037
2350
|
})
|
|
2038
2351
|
);
|
|
2039
2352
|
server.registerTool(
|
|
@@ -2073,8 +2386,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2073
2386
|
inputSchema: { id: idPathSegmentSchema, confirm: z23.literal(true).describe(confirmDescription) }
|
|
2074
2387
|
},
|
|
2075
2388
|
async ({ id }) => run(async () => {
|
|
2076
|
-
await client.delete(`/teams/${encodePathSegment(id)}`);
|
|
2077
|
-
return
|
|
2389
|
+
const response = await client.delete(`/teams/${encodePathSegment(id)}`);
|
|
2390
|
+
return okAfter(response, `Team "${id}" deleted.`);
|
|
2078
2391
|
})
|
|
2079
2392
|
);
|
|
2080
2393
|
server.registerTool(
|
|
@@ -2101,8 +2414,8 @@ function registerUsersTeamsTools(server, client) {
|
|
|
2101
2414
|
}
|
|
2102
2415
|
},
|
|
2103
2416
|
async ({ id, userId }) => run(async () => {
|
|
2104
|
-
await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
|
|
2105
|
-
return
|
|
2417
|
+
const response = await client.delete(`/teams/${encodePathSegment(id)}/members/${encodePathSegment(userId)}`);
|
|
2418
|
+
return okAfter(response, `User "${userId}" removed from team "${id}".`);
|
|
2106
2419
|
})
|
|
2107
2420
|
);
|
|
2108
2421
|
}
|
|
@@ -2131,7 +2444,33 @@ function registerWebhookTools(server, client) {
|
|
|
2131
2444
|
}
|
|
2132
2445
|
|
|
2133
2446
|
// src/tools/index.ts
|
|
2134
|
-
function registerAllTools(server, client) {
|
|
2447
|
+
function registerAllTools(server, client, options = {}) {
|
|
2448
|
+
const dispatcher = options.dispatcher === void 0 ? new GovernedDispatcher(client) : options.dispatcher;
|
|
2449
|
+
const target = dispatcher?.enabled ? withGovernedHandlers(server, dispatcher) : server;
|
|
2450
|
+
registerModules(target, client);
|
|
2451
|
+
}
|
|
2452
|
+
function withGovernedHandlers(server, dispatcher) {
|
|
2453
|
+
return new Proxy(server, {
|
|
2454
|
+
get(t, prop, receiver) {
|
|
2455
|
+
if (prop !== "registerTool") return Reflect.get(t, prop, receiver);
|
|
2456
|
+
return (name, config, handler) => {
|
|
2457
|
+
const binding = GOVERNED_TOOLS[name];
|
|
2458
|
+
let wrapped = handler;
|
|
2459
|
+
if (binding) {
|
|
2460
|
+
wrapped = async (args) => await dispatcher.dispatch(name, args, binding) ?? await handler(args);
|
|
2461
|
+
} else if (dispatcher.requiresGovernance && isMutationTool(name)) {
|
|
2462
|
+
wrapped = async () => dispatcher.refuseUngoverned(name);
|
|
2463
|
+
}
|
|
2464
|
+
return t.registerTool(
|
|
2465
|
+
name,
|
|
2466
|
+
config,
|
|
2467
|
+
wrapped
|
|
2468
|
+
);
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
function registerModules(server, client) {
|
|
2135
2474
|
registerCollectionTools(server, client);
|
|
2136
2475
|
registerFieldTools(server, client);
|
|
2137
2476
|
registerItemTools(server, client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumibase/mcp-server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.3",
|
|
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.
|
|
30
|
+
"zod": "^4.6.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^26.
|
|
33
|
+
"@types/node": "^26.6.2",
|
|
34
34
|
"tsup": "^8.5.1",
|
|
35
35
|
"typescript": "^5.6.2",
|
|
36
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^5.0.1"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist"
|