@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/README.md +73 -0
- package/dist/index.cjs +380 -53
- package/dist/index.js +380 -53
- package/package.json +18 -4
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
1610
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
30
|
+
"zod": "^4.5.4"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^26.
|
|
33
|
+
"@types/node": "^26.4.1",
|
|
34
34
|
"tsup": "^8.5.1",
|
|
35
35
|
"typescript": "^5.6.2",
|
|
36
|
-
"vitest": "^
|
|
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",
|