@reveldigital/mcp-graphql-proxy 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/query-builder.d.ts.map +1 -1
- package/dist/core/query-builder.js +37 -0
- package/dist/core/query-builder.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +64 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.js +784 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/types/schema.graphql +612 -29
package/dist/index.js
CHANGED
|
@@ -119,9 +119,11 @@ server.tool('build_query', 'Build an optimized GraphQL query with field selectio
|
|
|
119
119
|
'playlist', 'playlistGroups', 'schedule', 'scheduleGroups',
|
|
120
120
|
'template', 'templateGroups', 'user',
|
|
121
121
|
// Alerts & Monitoring
|
|
122
|
-
'alert',
|
|
122
|
+
'alert', 'alertRule',
|
|
123
123
|
// Audit & Compliance
|
|
124
124
|
'auditEvent',
|
|
125
|
+
// Connector Data
|
|
126
|
+
'connectorData',
|
|
125
127
|
// Data Tables
|
|
126
128
|
'dataTables', 'dataTable',
|
|
127
129
|
// Device Commands & Permissions
|
|
@@ -159,6 +161,11 @@ server.tool('build_query', 'Build an optimized GraphQL query with field selectio
|
|
|
159
161
|
// Alert-specific arguments
|
|
160
162
|
activeOnly: z.boolean().optional().describe('Filter to show only active alerts (alert only)'),
|
|
161
163
|
resolved: z.boolean().optional().describe('Filter by resolved state (alert only)'),
|
|
164
|
+
orgId: z.string().optional().describe('Filter by organization ID (alert, alertRule)'),
|
|
165
|
+
// Connector data arguments
|
|
166
|
+
datasourceId: z.string().optional().describe('Registered data source identifier (connectorData only)'),
|
|
167
|
+
datasetId: z.string().optional().describe('Registered dataset identifier on that source (connectorData only)'),
|
|
168
|
+
params: z.record(z.unknown()).optional().describe('Optional validated parameters for the dataset (connectorData only)'),
|
|
162
169
|
}, async (args) => {
|
|
163
170
|
try {
|
|
164
171
|
const queryArgs = {};
|
|
@@ -197,6 +204,15 @@ server.tool('build_query', 'Build an optimized GraphQL query with field selectio
|
|
|
197
204
|
queryArgs.activeOnly = args.activeOnly;
|
|
198
205
|
if (args.resolved !== undefined)
|
|
199
206
|
queryArgs.resolved = args.resolved;
|
|
207
|
+
if (args.orgId !== undefined)
|
|
208
|
+
queryArgs.orgId = args.orgId;
|
|
209
|
+
// Connector data arguments
|
|
210
|
+
if (args.datasourceId !== undefined)
|
|
211
|
+
queryArgs.datasourceId = args.datasourceId;
|
|
212
|
+
if (args.datasetId !== undefined)
|
|
213
|
+
queryArgs.datasetId = args.datasetId;
|
|
214
|
+
if (args.params !== undefined)
|
|
215
|
+
queryArgs.params = args.params;
|
|
200
216
|
const { query, variables } = QueryBuilder.build({
|
|
201
217
|
operation: args.operation,
|
|
202
218
|
fields: args.fields ?? [],
|
|
@@ -1153,6 +1169,753 @@ mutation ReorderPlaylistSources($input: ReorderPlaylistSourcesInput) {
|
|
|
1153
1169
|
}
|
|
1154
1170
|
});
|
|
1155
1171
|
// ============================================================================
|
|
1172
|
+
// Tool: Update Template
|
|
1173
|
+
// ============================================================================
|
|
1174
|
+
server.tool('update_template', "Update a template's properties (name, group, tags, dimensions, colors, orientation, script). Only provided fields are changed. Example: AI adjusts template dimensions to optimize for a new display resolution.", {
|
|
1175
|
+
templateId: z.string().describe('The template ID to update'),
|
|
1176
|
+
name: z.string().optional().describe('Template name'),
|
|
1177
|
+
groupId: z.string().optional().describe('Group ID to assign the template to'),
|
|
1178
|
+
tags: z.string().optional().describe('Tags (newline-delimited values stored in the Description field)'),
|
|
1179
|
+
script: z.string().optional().describe('Template script for custom logic'),
|
|
1180
|
+
backColor: z.string().optional().describe("Background color (e.g., '#000000')"),
|
|
1181
|
+
width: z.number().optional().describe('Template width in pixels'),
|
|
1182
|
+
height: z.number().optional().describe('Template height in pixels'),
|
|
1183
|
+
orientation: z.number().optional().describe('Display orientation (0=Landscape, 1=Portrait, 2=Landscape Reversed, 3=Portrait Reversed)'),
|
|
1184
|
+
}, async (args) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const mutation = `
|
|
1187
|
+
mutation UpdateTemplate($input: UpdateTemplateInput) {
|
|
1188
|
+
updateTemplate(input: $input) {
|
|
1189
|
+
success
|
|
1190
|
+
template {
|
|
1191
|
+
id
|
|
1192
|
+
name
|
|
1193
|
+
width
|
|
1194
|
+
height
|
|
1195
|
+
orientation
|
|
1196
|
+
modules {
|
|
1197
|
+
id
|
|
1198
|
+
name
|
|
1199
|
+
type
|
|
1200
|
+
left
|
|
1201
|
+
top
|
|
1202
|
+
width
|
|
1203
|
+
height
|
|
1204
|
+
playlistId
|
|
1205
|
+
sequence
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
error
|
|
1209
|
+
}
|
|
1210
|
+
}`;
|
|
1211
|
+
const input = { templateId: args.templateId };
|
|
1212
|
+
if (args.name !== undefined)
|
|
1213
|
+
input.name = args.name;
|
|
1214
|
+
if (args.groupId !== undefined)
|
|
1215
|
+
input.groupId = args.groupId;
|
|
1216
|
+
if (args.tags !== undefined)
|
|
1217
|
+
input.tags = args.tags;
|
|
1218
|
+
if (args.script !== undefined)
|
|
1219
|
+
input.script = args.script;
|
|
1220
|
+
if (args.backColor !== undefined)
|
|
1221
|
+
input.backColor = args.backColor;
|
|
1222
|
+
if (args.width !== undefined)
|
|
1223
|
+
input.width = args.width;
|
|
1224
|
+
if (args.height !== undefined)
|
|
1225
|
+
input.height = args.height;
|
|
1226
|
+
if (args.orientation !== undefined)
|
|
1227
|
+
input.orientation = args.orientation;
|
|
1228
|
+
const response = await graphqlClient.query(mutation, { input });
|
|
1229
|
+
if (response.errors && response.errors.length > 0) {
|
|
1230
|
+
const result = {
|
|
1231
|
+
success: false,
|
|
1232
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1233
|
+
};
|
|
1234
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1235
|
+
}
|
|
1236
|
+
const result = {
|
|
1237
|
+
success: true,
|
|
1238
|
+
data: response.data,
|
|
1239
|
+
};
|
|
1240
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
const result = {
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1246
|
+
};
|
|
1247
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
// ============================================================================
|
|
1251
|
+
// Tool: Add Template Module
|
|
1252
|
+
// ============================================================================
|
|
1253
|
+
server.tool('add_template_module', "Add a new module (zone/region) to a template. Modules define rectangular areas where content from a playlist is displayed. Example: AI adds a new weather module to show real-time conditions in the corner.", {
|
|
1254
|
+
templateId: z.string().describe('The template ID to add the module to'),
|
|
1255
|
+
module: z.object({
|
|
1256
|
+
name: z.string().optional().describe('Module/zone name'),
|
|
1257
|
+
type: z.string().optional().describe("Module/zone type (e.g., 'Playlist', 'Clock', 'Weather', 'Ticker', 'Web')"),
|
|
1258
|
+
left: z.number().optional().describe('Left position in pixels'),
|
|
1259
|
+
top: z.number().optional().describe('Top position in pixels'),
|
|
1260
|
+
width: z.number().optional().describe('Width in pixels'),
|
|
1261
|
+
height: z.number().optional().describe('Height in pixels'),
|
|
1262
|
+
playlistId: z.string().optional().describe('Playlist ID assigned to this module'),
|
|
1263
|
+
sequence: z.number().optional().describe('Z-order/sequence for overlapping modules (higher appears in front)'),
|
|
1264
|
+
}).describe('The module to add'),
|
|
1265
|
+
}, async (args) => {
|
|
1266
|
+
try {
|
|
1267
|
+
const mutation = `
|
|
1268
|
+
mutation AddTemplateModule($input: AddTemplateModuleInput) {
|
|
1269
|
+
addTemplateModule(input: $input) {
|
|
1270
|
+
success
|
|
1271
|
+
module {
|
|
1272
|
+
id
|
|
1273
|
+
name
|
|
1274
|
+
type
|
|
1275
|
+
left
|
|
1276
|
+
top
|
|
1277
|
+
width
|
|
1278
|
+
height
|
|
1279
|
+
playlistId
|
|
1280
|
+
sequence
|
|
1281
|
+
options {
|
|
1282
|
+
id
|
|
1283
|
+
name
|
|
1284
|
+
value
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
error
|
|
1288
|
+
}
|
|
1289
|
+
}`;
|
|
1290
|
+
const response = await graphqlClient.query(mutation, {
|
|
1291
|
+
input: {
|
|
1292
|
+
templateId: args.templateId,
|
|
1293
|
+
module: args.module,
|
|
1294
|
+
},
|
|
1295
|
+
});
|
|
1296
|
+
if (response.errors && response.errors.length > 0) {
|
|
1297
|
+
const result = {
|
|
1298
|
+
success: false,
|
|
1299
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1300
|
+
};
|
|
1301
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1302
|
+
}
|
|
1303
|
+
const result = {
|
|
1304
|
+
success: true,
|
|
1305
|
+
data: response.data,
|
|
1306
|
+
};
|
|
1307
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
const result = {
|
|
1311
|
+
success: false,
|
|
1312
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1313
|
+
};
|
|
1314
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
// ============================================================================
|
|
1318
|
+
// Tool: Update Template Module
|
|
1319
|
+
// ============================================================================
|
|
1320
|
+
server.tool('update_template_module', 'Update a module (zone/region) within a template. Only provided fields are changed. Example: AI repositions a module based on engagement analytics, or changes the assigned playlist.', {
|
|
1321
|
+
templateId: z.string().describe('The template ID containing the module'),
|
|
1322
|
+
moduleId: z.string().describe('The module ID to update'),
|
|
1323
|
+
name: z.string().optional().describe('Module/zone name'),
|
|
1324
|
+
type: z.string().optional().describe("Module/zone type (e.g., 'Playlist', 'Clock', 'Weather', 'Ticker', 'Web')"),
|
|
1325
|
+
left: z.number().optional().describe('Left position in pixels'),
|
|
1326
|
+
top: z.number().optional().describe('Top position in pixels'),
|
|
1327
|
+
width: z.number().optional().describe('Width in pixels'),
|
|
1328
|
+
height: z.number().optional().describe('Height in pixels'),
|
|
1329
|
+
playlistId: z.string().optional().describe('Playlist ID assigned to this module'),
|
|
1330
|
+
sequence: z.number().optional().describe('Z-order/sequence for overlapping modules'),
|
|
1331
|
+
}, async (args) => {
|
|
1332
|
+
try {
|
|
1333
|
+
const mutation = `
|
|
1334
|
+
mutation UpdateTemplateModule($input: UpdateTemplateModuleInput) {
|
|
1335
|
+
updateTemplateModule(input: $input) {
|
|
1336
|
+
success
|
|
1337
|
+
module {
|
|
1338
|
+
id
|
|
1339
|
+
name
|
|
1340
|
+
type
|
|
1341
|
+
left
|
|
1342
|
+
top
|
|
1343
|
+
width
|
|
1344
|
+
height
|
|
1345
|
+
playlistId
|
|
1346
|
+
sequence
|
|
1347
|
+
options {
|
|
1348
|
+
id
|
|
1349
|
+
name
|
|
1350
|
+
value
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
error
|
|
1354
|
+
}
|
|
1355
|
+
}`;
|
|
1356
|
+
const input = {
|
|
1357
|
+
templateId: args.templateId,
|
|
1358
|
+
moduleId: args.moduleId,
|
|
1359
|
+
};
|
|
1360
|
+
if (args.name !== undefined)
|
|
1361
|
+
input.name = args.name;
|
|
1362
|
+
if (args.type !== undefined)
|
|
1363
|
+
input.type = args.type;
|
|
1364
|
+
if (args.left !== undefined)
|
|
1365
|
+
input.left = args.left;
|
|
1366
|
+
if (args.top !== undefined)
|
|
1367
|
+
input.top = args.top;
|
|
1368
|
+
if (args.width !== undefined)
|
|
1369
|
+
input.width = args.width;
|
|
1370
|
+
if (args.height !== undefined)
|
|
1371
|
+
input.height = args.height;
|
|
1372
|
+
if (args.playlistId !== undefined)
|
|
1373
|
+
input.playlistId = args.playlistId;
|
|
1374
|
+
if (args.sequence !== undefined)
|
|
1375
|
+
input.sequence = args.sequence;
|
|
1376
|
+
const response = await graphqlClient.query(mutation, { input });
|
|
1377
|
+
if (response.errors && response.errors.length > 0) {
|
|
1378
|
+
const result = {
|
|
1379
|
+
success: false,
|
|
1380
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1381
|
+
};
|
|
1382
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1383
|
+
}
|
|
1384
|
+
const result = {
|
|
1385
|
+
success: true,
|
|
1386
|
+
data: response.data,
|
|
1387
|
+
};
|
|
1388
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
const result = {
|
|
1392
|
+
success: false,
|
|
1393
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1394
|
+
};
|
|
1395
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
// ============================================================================
|
|
1399
|
+
// Tool: Remove Template Module
|
|
1400
|
+
// ============================================================================
|
|
1401
|
+
server.tool('remove_template_module', 'Remove a module (zone/region) from a template by module ID. Example: AI removes an underperforming ad module to optimize content flow.', {
|
|
1402
|
+
templateId: z.string().describe('The template ID containing the module'),
|
|
1403
|
+
moduleId: z.string().describe('The module ID to remove'),
|
|
1404
|
+
}, async (args) => {
|
|
1405
|
+
try {
|
|
1406
|
+
const mutation = `
|
|
1407
|
+
mutation RemoveTemplateModule($input: RemoveTemplateModuleInput) {
|
|
1408
|
+
removeTemplateModule(input: $input) {
|
|
1409
|
+
success
|
|
1410
|
+
module {
|
|
1411
|
+
id
|
|
1412
|
+
name
|
|
1413
|
+
type
|
|
1414
|
+
}
|
|
1415
|
+
error
|
|
1416
|
+
}
|
|
1417
|
+
}`;
|
|
1418
|
+
const response = await graphqlClient.query(mutation, {
|
|
1419
|
+
input: {
|
|
1420
|
+
templateId: args.templateId,
|
|
1421
|
+
moduleId: args.moduleId,
|
|
1422
|
+
},
|
|
1423
|
+
});
|
|
1424
|
+
if (response.errors && response.errors.length > 0) {
|
|
1425
|
+
const result = {
|
|
1426
|
+
success: false,
|
|
1427
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1428
|
+
};
|
|
1429
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1430
|
+
}
|
|
1431
|
+
const result = {
|
|
1432
|
+
success: true,
|
|
1433
|
+
data: response.data,
|
|
1434
|
+
};
|
|
1435
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1436
|
+
}
|
|
1437
|
+
catch (error) {
|
|
1438
|
+
const result = {
|
|
1439
|
+
success: false,
|
|
1440
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1441
|
+
};
|
|
1442
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
// ============================================================================
|
|
1446
|
+
// Tool: Reorder Template Modules
|
|
1447
|
+
// ============================================================================
|
|
1448
|
+
server.tool('reorder_template_modules', 'Reorder modules (zones) within a template by updating their sequence/z-order. Higher sequence values appear in front. Example: AI reorders modules to bring important content to the front based on viewer engagement.', {
|
|
1449
|
+
templateId: z.string().describe('The template ID'),
|
|
1450
|
+
moduleSequences: z.array(z.object({
|
|
1451
|
+
moduleId: z.string().describe('The module ID'),
|
|
1452
|
+
sequence: z.number().describe('The new sequence/z-order value'),
|
|
1453
|
+
})).describe('List of module IDs with their new sequence numbers'),
|
|
1454
|
+
}, async (args) => {
|
|
1455
|
+
try {
|
|
1456
|
+
const mutation = `
|
|
1457
|
+
mutation ReorderTemplateModules($input: ReorderTemplateModulesInput) {
|
|
1458
|
+
reorderTemplateModules(input: $input) {
|
|
1459
|
+
success
|
|
1460
|
+
template {
|
|
1461
|
+
id
|
|
1462
|
+
name
|
|
1463
|
+
modules {
|
|
1464
|
+
id
|
|
1465
|
+
name
|
|
1466
|
+
type
|
|
1467
|
+
sequence
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
error
|
|
1471
|
+
}
|
|
1472
|
+
}`;
|
|
1473
|
+
const response = await graphqlClient.query(mutation, {
|
|
1474
|
+
input: {
|
|
1475
|
+
templateId: args.templateId,
|
|
1476
|
+
moduleSequences: args.moduleSequences,
|
|
1477
|
+
},
|
|
1478
|
+
});
|
|
1479
|
+
if (response.errors && response.errors.length > 0) {
|
|
1480
|
+
const result = {
|
|
1481
|
+
success: false,
|
|
1482
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1483
|
+
};
|
|
1484
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1485
|
+
}
|
|
1486
|
+
const result = {
|
|
1487
|
+
success: true,
|
|
1488
|
+
data: response.data,
|
|
1489
|
+
};
|
|
1490
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
const result = {
|
|
1494
|
+
success: false,
|
|
1495
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1496
|
+
};
|
|
1497
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
// ============================================================================
|
|
1501
|
+
// Tool: Batch Update Template Modules
|
|
1502
|
+
// ============================================================================
|
|
1503
|
+
server.tool('batch_update_template_modules', 'Batch update multiple modules in a template at once. Each entry must include the moduleId; only provided fields are changed. Example: AI repositions all modules to create a new layout optimized for viewer attention.', {
|
|
1504
|
+
templateId: z.string().describe('The template ID'),
|
|
1505
|
+
modules: z.array(z.object({
|
|
1506
|
+
moduleId: z.string().describe('The module ID to update'),
|
|
1507
|
+
name: z.string().optional().describe('Module/zone name'),
|
|
1508
|
+
type: z.string().optional().describe('Module/zone type'),
|
|
1509
|
+
left: z.number().optional().describe('Left position in pixels'),
|
|
1510
|
+
top: z.number().optional().describe('Top position in pixels'),
|
|
1511
|
+
width: z.number().optional().describe('Width in pixels'),
|
|
1512
|
+
height: z.number().optional().describe('Height in pixels'),
|
|
1513
|
+
playlistId: z.string().optional().describe('Playlist ID assigned to this module'),
|
|
1514
|
+
sequence: z.number().optional().describe('Z-order/sequence'),
|
|
1515
|
+
})).describe('List of module updates to apply (each requires moduleId)'),
|
|
1516
|
+
}, async (args) => {
|
|
1517
|
+
try {
|
|
1518
|
+
const mutation = `
|
|
1519
|
+
mutation BatchUpdateTemplateModules($input: BatchUpdateTemplateModulesInput) {
|
|
1520
|
+
batchUpdateTemplateModules(input: $input) {
|
|
1521
|
+
totalModules
|
|
1522
|
+
successCount
|
|
1523
|
+
failureCount
|
|
1524
|
+
results {
|
|
1525
|
+
success
|
|
1526
|
+
module {
|
|
1527
|
+
id
|
|
1528
|
+
name
|
|
1529
|
+
type
|
|
1530
|
+
left
|
|
1531
|
+
top
|
|
1532
|
+
width
|
|
1533
|
+
height
|
|
1534
|
+
playlistId
|
|
1535
|
+
sequence
|
|
1536
|
+
}
|
|
1537
|
+
error
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}`;
|
|
1541
|
+
const modules = args.modules.map((m) => ({ ...m, templateId: args.templateId }));
|
|
1542
|
+
const response = await graphqlClient.query(mutation, {
|
|
1543
|
+
input: {
|
|
1544
|
+
templateId: args.templateId,
|
|
1545
|
+
modules,
|
|
1546
|
+
},
|
|
1547
|
+
});
|
|
1548
|
+
if (response.errors && response.errors.length > 0) {
|
|
1549
|
+
const result = {
|
|
1550
|
+
success: false,
|
|
1551
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1552
|
+
};
|
|
1553
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1554
|
+
}
|
|
1555
|
+
const result = {
|
|
1556
|
+
success: true,
|
|
1557
|
+
data: response.data,
|
|
1558
|
+
};
|
|
1559
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
const result = {
|
|
1563
|
+
success: false,
|
|
1564
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1565
|
+
};
|
|
1566
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
// ============================================================================
|
|
1570
|
+
// Tool: Set Module Options
|
|
1571
|
+
// ============================================================================
|
|
1572
|
+
server.tool('set_module_options', 'Set all configuration options for a module/zone (replaces existing options). Options are key-value pairs storing type-specific settings. Example: AI configures a weather module with location, units, and API key settings.', {
|
|
1573
|
+
templateId: z.string().describe('The template ID'),
|
|
1574
|
+
moduleId: z.string().describe('The module ID'),
|
|
1575
|
+
options: z.array(z.object({
|
|
1576
|
+
name: z.string().describe("Option name/key (e.g., 'timezone', 'location', 'url')"),
|
|
1577
|
+
value: z.string().describe('Option value (string representation, parsed by option type)'),
|
|
1578
|
+
})).describe('List of options to set (replaces all existing options)'),
|
|
1579
|
+
}, async (args) => {
|
|
1580
|
+
try {
|
|
1581
|
+
const mutation = `
|
|
1582
|
+
mutation SetModuleOptions($input: SetModuleOptionsInput) {
|
|
1583
|
+
setModuleOptions(input: $input) {
|
|
1584
|
+
success
|
|
1585
|
+
module {
|
|
1586
|
+
id
|
|
1587
|
+
name
|
|
1588
|
+
type
|
|
1589
|
+
options {
|
|
1590
|
+
id
|
|
1591
|
+
name
|
|
1592
|
+
value
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
error
|
|
1596
|
+
}
|
|
1597
|
+
}`;
|
|
1598
|
+
const response = await graphqlClient.query(mutation, {
|
|
1599
|
+
input: {
|
|
1600
|
+
templateId: args.templateId,
|
|
1601
|
+
moduleId: args.moduleId,
|
|
1602
|
+
options: args.options,
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
if (response.errors && response.errors.length > 0) {
|
|
1606
|
+
const result = {
|
|
1607
|
+
success: false,
|
|
1608
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1609
|
+
};
|
|
1610
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1611
|
+
}
|
|
1612
|
+
const result = {
|
|
1613
|
+
success: true,
|
|
1614
|
+
data: response.data,
|
|
1615
|
+
};
|
|
1616
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1617
|
+
}
|
|
1618
|
+
catch (error) {
|
|
1619
|
+
const result = {
|
|
1620
|
+
success: false,
|
|
1621
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1622
|
+
};
|
|
1623
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
// ============================================================================
|
|
1627
|
+
// Tool: Upsert Module Option
|
|
1628
|
+
// ============================================================================
|
|
1629
|
+
server.tool('upsert_module_option', 'Add or update a single module/zone option (matched by name). Example: AI updates the refresh interval for a web module.', {
|
|
1630
|
+
templateId: z.string().describe('The template ID'),
|
|
1631
|
+
moduleId: z.string().describe('The module ID'),
|
|
1632
|
+
option: z.object({
|
|
1633
|
+
name: z.string().describe("Option name/key (e.g., 'timezone', 'location', 'url')"),
|
|
1634
|
+
value: z.string().describe('Option value (string representation, parsed by option type)'),
|
|
1635
|
+
}).describe('The option to create or update (matched by name)'),
|
|
1636
|
+
}, async (args) => {
|
|
1637
|
+
try {
|
|
1638
|
+
const mutation = `
|
|
1639
|
+
mutation UpsertModuleOption($input: UpsertModuleOptionInput) {
|
|
1640
|
+
upsertModuleOption(input: $input) {
|
|
1641
|
+
success
|
|
1642
|
+
option {
|
|
1643
|
+
id
|
|
1644
|
+
name
|
|
1645
|
+
value
|
|
1646
|
+
}
|
|
1647
|
+
error
|
|
1648
|
+
}
|
|
1649
|
+
}`;
|
|
1650
|
+
const response = await graphqlClient.query(mutation, {
|
|
1651
|
+
input: {
|
|
1652
|
+
templateId: args.templateId,
|
|
1653
|
+
moduleId: args.moduleId,
|
|
1654
|
+
option: args.option,
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
if (response.errors && response.errors.length > 0) {
|
|
1658
|
+
const result = {
|
|
1659
|
+
success: false,
|
|
1660
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1661
|
+
};
|
|
1662
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1663
|
+
}
|
|
1664
|
+
const result = {
|
|
1665
|
+
success: true,
|
|
1666
|
+
data: response.data,
|
|
1667
|
+
};
|
|
1668
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1669
|
+
}
|
|
1670
|
+
catch (error) {
|
|
1671
|
+
const result = {
|
|
1672
|
+
success: false,
|
|
1673
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1674
|
+
};
|
|
1675
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
// ============================================================================
|
|
1679
|
+
// Tool: Remove Module Option
|
|
1680
|
+
// ============================================================================
|
|
1681
|
+
server.tool('remove_module_option', 'Remove a module/zone option by name. Example: AI removes an outdated configuration setting from a module.', {
|
|
1682
|
+
templateId: z.string().describe('The template ID'),
|
|
1683
|
+
moduleId: z.string().describe('The module ID'),
|
|
1684
|
+
optionName: z.string().describe('The option name to remove'),
|
|
1685
|
+
}, async (args) => {
|
|
1686
|
+
try {
|
|
1687
|
+
const mutation = `
|
|
1688
|
+
mutation RemoveModuleOption($input: RemoveModuleOptionInput) {
|
|
1689
|
+
removeModuleOption(input: $input) {
|
|
1690
|
+
success
|
|
1691
|
+
option {
|
|
1692
|
+
id
|
|
1693
|
+
name
|
|
1694
|
+
value
|
|
1695
|
+
}
|
|
1696
|
+
error
|
|
1697
|
+
}
|
|
1698
|
+
}`;
|
|
1699
|
+
const response = await graphqlClient.query(mutation, {
|
|
1700
|
+
input: {
|
|
1701
|
+
templateId: args.templateId,
|
|
1702
|
+
moduleId: args.moduleId,
|
|
1703
|
+
optionName: args.optionName,
|
|
1704
|
+
},
|
|
1705
|
+
});
|
|
1706
|
+
if (response.errors && response.errors.length > 0) {
|
|
1707
|
+
const result = {
|
|
1708
|
+
success: false,
|
|
1709
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1710
|
+
};
|
|
1711
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1712
|
+
}
|
|
1713
|
+
const result = {
|
|
1714
|
+
success: true,
|
|
1715
|
+
data: response.data,
|
|
1716
|
+
};
|
|
1717
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1718
|
+
}
|
|
1719
|
+
catch (error) {
|
|
1720
|
+
const result = {
|
|
1721
|
+
success: false,
|
|
1722
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1723
|
+
};
|
|
1724
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
// ============================================================================
|
|
1728
|
+
// Tools: Alert Rules (create / update / delete)
|
|
1729
|
+
// ============================================================================
|
|
1730
|
+
// The API takes ruleSet as a serialized JSON string, not a nested object. Condition
|
|
1731
|
+
// types and operators are case-sensitive and validated server-side.
|
|
1732
|
+
const ALERT_RULE_CONDITION_TYPES = [
|
|
1733
|
+
'TimeOffline', 'LastUpdate', 'MemoryUsage', 'CpuUsage', 'DiskUsage', 'NoContent',
|
|
1734
|
+
'BytesTxDay', 'BytesRxDay', 'BytesTotalDay', 'BytesTxPeriod', 'BytesRxPeriod',
|
|
1735
|
+
'BytesTotalPeriod', 'ClockSkew', 'DisplayDetected', 'BatteryVoltage',
|
|
1736
|
+
'BatteryPercentage', 'SystemTemperature',
|
|
1737
|
+
];
|
|
1738
|
+
const RULE_SET_DESCRIPTION = [
|
|
1739
|
+
'Rule set configuration as a SERIALIZED JSON STRING (not a nested object).',
|
|
1740
|
+
'Shape: an array of condition groups combined with logical AND; the `items` within each group are combined with logical OR.',
|
|
1741
|
+
'Each item is {"type", "op", "value"}:',
|
|
1742
|
+
`\`type\` (required, case-sensitive) one of: ${ALERT_RULE_CONDITION_TYPES.join(', ')}.`,
|
|
1743
|
+
'`op` (optional) one of EQ, NE, GT, LT, GTE, LTE (defaults to EQ).',
|
|
1744
|
+
'`value` (required) a non-empty array of numbers.',
|
|
1745
|
+
'Example: [{"items":[{"type":"CpuUsage","op":"GTE","value":[90]}]}]',
|
|
1746
|
+
'Invalid rule sets are rejected. Null or empty leaves the rule unevaluated.',
|
|
1747
|
+
].join(' ');
|
|
1748
|
+
const ALERT_RULE_SELECTION = `
|
|
1749
|
+
success
|
|
1750
|
+
alertRule {
|
|
1751
|
+
id
|
|
1752
|
+
name
|
|
1753
|
+
ruleSet
|
|
1754
|
+
isEnabled
|
|
1755
|
+
period
|
|
1756
|
+
threshold
|
|
1757
|
+
webhookUrl
|
|
1758
|
+
allDevices
|
|
1759
|
+
allUsers
|
|
1760
|
+
deviceIds
|
|
1761
|
+
userIds
|
|
1762
|
+
createdOn
|
|
1763
|
+
updatedOn
|
|
1764
|
+
}
|
|
1765
|
+
error`;
|
|
1766
|
+
server.tool('create_alert_rule', 'Create a new alert rule — the conditions that trigger alerts. Example: create a rule that fires when a lobby display goes offline. Use build_query with the alertRule operation to read existing rules first.', {
|
|
1767
|
+
name: z.string().describe('The name of the alert rule'),
|
|
1768
|
+
ruleSet: z.string().optional().describe(RULE_SET_DESCRIPTION),
|
|
1769
|
+
isEnabled: z.boolean().optional().describe('Whether the rule is enabled and will be evaluated. Disabled rules are stored but never trigger.'),
|
|
1770
|
+
period: z.number().optional().describe('The look-back / evaluation period, in MINUTES, used by time-window based conditions such as NoContent. Ignored by conditions that read the current value (e.g. CpuUsage).'),
|
|
1771
|
+
threshold: z.number().optional().describe('Legacy scalar threshold. Prefer expressing thresholds inside ruleSet.'),
|
|
1772
|
+
webhookUrl: z.string().optional().describe('Optional HTTPS webhook URL invoked (HTTP POST) when the rule triggers'),
|
|
1773
|
+
allDevices: z.boolean().optional().describe('If true, the rule is evaluated against every device in the account and deviceIds is ignored'),
|
|
1774
|
+
allUsers: z.boolean().optional().describe('If true, all users in the account are notified and userIds is ignored'),
|
|
1775
|
+
deviceIds: z.array(z.string()).optional().describe('Devices to associate with the rule. Ignored when allDevices is true.'),
|
|
1776
|
+
userIds: z.array(z.string()).optional().describe('Users to notify. Ignored when allUsers is true.'),
|
|
1777
|
+
}, async (args) => {
|
|
1778
|
+
try {
|
|
1779
|
+
const mutation = `
|
|
1780
|
+
mutation CreateAlertRule($input: AlertRuleInput) {
|
|
1781
|
+
createAlertRule(input: $input) {${ALERT_RULE_SELECTION}
|
|
1782
|
+
}
|
|
1783
|
+
}`;
|
|
1784
|
+
const input = { name: args.name };
|
|
1785
|
+
if (args.ruleSet !== undefined)
|
|
1786
|
+
input.ruleSet = args.ruleSet;
|
|
1787
|
+
if (args.isEnabled !== undefined)
|
|
1788
|
+
input.isEnabled = args.isEnabled;
|
|
1789
|
+
if (args.period !== undefined)
|
|
1790
|
+
input.period = args.period;
|
|
1791
|
+
if (args.threshold !== undefined)
|
|
1792
|
+
input.threshold = args.threshold;
|
|
1793
|
+
if (args.webhookUrl !== undefined)
|
|
1794
|
+
input.webhookUrl = args.webhookUrl;
|
|
1795
|
+
if (args.allDevices !== undefined)
|
|
1796
|
+
input.allDevices = args.allDevices;
|
|
1797
|
+
if (args.allUsers !== undefined)
|
|
1798
|
+
input.allUsers = args.allUsers;
|
|
1799
|
+
if (args.deviceIds !== undefined)
|
|
1800
|
+
input.deviceIds = args.deviceIds;
|
|
1801
|
+
if (args.userIds !== undefined)
|
|
1802
|
+
input.userIds = args.userIds;
|
|
1803
|
+
const response = await graphqlClient.query(mutation, { input });
|
|
1804
|
+
if (response.errors && response.errors.length > 0) {
|
|
1805
|
+
const result = {
|
|
1806
|
+
success: false,
|
|
1807
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1808
|
+
};
|
|
1809
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1810
|
+
}
|
|
1811
|
+
const result = {
|
|
1812
|
+
success: true,
|
|
1813
|
+
data: response.data,
|
|
1814
|
+
};
|
|
1815
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1816
|
+
}
|
|
1817
|
+
catch (error) {
|
|
1818
|
+
const result = {
|
|
1819
|
+
success: false,
|
|
1820
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1821
|
+
};
|
|
1822
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
server.tool('update_alert_rule', 'Update an existing alert rule. Only provided fields are changed. Example: disable a noisy rule, or widen its device scope.', {
|
|
1826
|
+
id: z.string().describe('The alert rule ID to update'),
|
|
1827
|
+
name: z.string().optional().describe('The name of the alert rule'),
|
|
1828
|
+
ruleSet: z.string().optional().describe(RULE_SET_DESCRIPTION),
|
|
1829
|
+
isEnabled: z.boolean().optional().describe('Whether the rule is enabled and will be evaluated'),
|
|
1830
|
+
period: z.number().optional().describe('The look-back / evaluation period, in MINUTES, used by time-window based conditions such as NoContent. Ignored by conditions that read the current value (e.g. CpuUsage).'),
|
|
1831
|
+
threshold: z.number().optional().describe('Legacy scalar threshold. Prefer expressing thresholds inside ruleSet.'),
|
|
1832
|
+
webhookUrl: z.string().optional().describe('Optional HTTPS webhook URL invoked when the rule triggers'),
|
|
1833
|
+
allDevices: z.boolean().optional().describe('If true, the rule is evaluated against every device and deviceIds is ignored'),
|
|
1834
|
+
allUsers: z.boolean().optional().describe('If true, all users are notified and userIds is ignored'),
|
|
1835
|
+
deviceIds: z.array(z.string()).optional().describe('Devices to associate with the rule. Ignored when allDevices is true.'),
|
|
1836
|
+
userIds: z.array(z.string()).optional().describe('Users to notify. Ignored when allUsers is true.'),
|
|
1837
|
+
}, async (args) => {
|
|
1838
|
+
try {
|
|
1839
|
+
const mutation = `
|
|
1840
|
+
mutation UpdateAlertRule($input: AlertRuleInput) {
|
|
1841
|
+
updateAlertRule(input: $input) {${ALERT_RULE_SELECTION}
|
|
1842
|
+
}
|
|
1843
|
+
}`;
|
|
1844
|
+
const input = { id: args.id };
|
|
1845
|
+
if (args.name !== undefined)
|
|
1846
|
+
input.name = args.name;
|
|
1847
|
+
if (args.ruleSet !== undefined)
|
|
1848
|
+
input.ruleSet = args.ruleSet;
|
|
1849
|
+
if (args.isEnabled !== undefined)
|
|
1850
|
+
input.isEnabled = args.isEnabled;
|
|
1851
|
+
if (args.period !== undefined)
|
|
1852
|
+
input.period = args.period;
|
|
1853
|
+
if (args.threshold !== undefined)
|
|
1854
|
+
input.threshold = args.threshold;
|
|
1855
|
+
if (args.webhookUrl !== undefined)
|
|
1856
|
+
input.webhookUrl = args.webhookUrl;
|
|
1857
|
+
if (args.allDevices !== undefined)
|
|
1858
|
+
input.allDevices = args.allDevices;
|
|
1859
|
+
if (args.allUsers !== undefined)
|
|
1860
|
+
input.allUsers = args.allUsers;
|
|
1861
|
+
if (args.deviceIds !== undefined)
|
|
1862
|
+
input.deviceIds = args.deviceIds;
|
|
1863
|
+
if (args.userIds !== undefined)
|
|
1864
|
+
input.userIds = args.userIds;
|
|
1865
|
+
const response = await graphqlClient.query(mutation, { input });
|
|
1866
|
+
if (response.errors && response.errors.length > 0) {
|
|
1867
|
+
const result = {
|
|
1868
|
+
success: false,
|
|
1869
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1870
|
+
};
|
|
1871
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1872
|
+
}
|
|
1873
|
+
const result = {
|
|
1874
|
+
success: true,
|
|
1875
|
+
data: response.data,
|
|
1876
|
+
};
|
|
1877
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1878
|
+
}
|
|
1879
|
+
catch (error) {
|
|
1880
|
+
const result = {
|
|
1881
|
+
success: false,
|
|
1882
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1883
|
+
};
|
|
1884
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
server.tool('delete_alert_rule', 'Delete an alert rule by ID. This stops the rule from ever triggering again — consider update_alert_rule with isEnabled=false if the rule may be needed later.', {
|
|
1888
|
+
id: z.string().describe('The alert rule ID to delete'),
|
|
1889
|
+
}, async (args) => {
|
|
1890
|
+
try {
|
|
1891
|
+
const mutation = `
|
|
1892
|
+
mutation DeleteAlertRule($id: String) {
|
|
1893
|
+
deleteAlertRule(id: $id) {${ALERT_RULE_SELECTION}
|
|
1894
|
+
}
|
|
1895
|
+
}`;
|
|
1896
|
+
const response = await graphqlClient.query(mutation, { id: args.id });
|
|
1897
|
+
if (response.errors && response.errors.length > 0) {
|
|
1898
|
+
const result = {
|
|
1899
|
+
success: false,
|
|
1900
|
+
error: response.errors.map((e) => e.message).join('; '),
|
|
1901
|
+
};
|
|
1902
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1903
|
+
}
|
|
1904
|
+
const result = {
|
|
1905
|
+
success: true,
|
|
1906
|
+
data: response.data,
|
|
1907
|
+
};
|
|
1908
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1909
|
+
}
|
|
1910
|
+
catch (error) {
|
|
1911
|
+
const result = {
|
|
1912
|
+
success: false,
|
|
1913
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1914
|
+
};
|
|
1915
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
// ============================================================================
|
|
1156
1919
|
// Tool: List Data Tables
|
|
1157
1920
|
// ============================================================================
|
|
1158
1921
|
server.tool('list_data_tables', 'List data tables in the account. Returns table summaries with pagination. Use to discover available tables before querying rows.', {
|
|
@@ -2142,6 +2905,10 @@ server.resource('schema://reveldigital/graphql', 'Revel Digital GraphQL Schema',
|
|
|
2142
2905
|
|
|
2143
2906
|
### Alerts & Monitoring
|
|
2144
2907
|
- alert - Query system alerts for device issues and rule violations
|
|
2908
|
+
- alertRule - Query alert rules (the conditions that trigger alerts)
|
|
2909
|
+
|
|
2910
|
+
### Connector Data
|
|
2911
|
+
- connectorData - Fetch a normalized dataset from a registered connector data source (datasourceId + datasetId + params)
|
|
2145
2912
|
|
|
2146
2913
|
### Analytics (require date range)
|
|
2147
2914
|
Aggregated metrics optimized for AI context windows:
|
|
@@ -2171,6 +2938,11 @@ Raw logs return too much data for AI context windows.
|
|
|
2171
2938
|
- send_device_command - Send commands to a specific device
|
|
2172
2939
|
- send_bulk_device_commands - Send commands to multiple devices at once
|
|
2173
2940
|
|
|
2941
|
+
### Alert Rule Management
|
|
2942
|
+
- create_alert_rule - Create a new alert rule (conditions that trigger alerts)
|
|
2943
|
+
- update_alert_rule - Update an existing alert rule (partial update)
|
|
2944
|
+
- delete_alert_rule - Delete an alert rule
|
|
2945
|
+
|
|
2174
2946
|
### Group Management
|
|
2175
2947
|
- create_device_group - Create a new device group (optional parentId for nesting)
|
|
2176
2948
|
- create_media_group - Create a new media group (optional parentId for nesting)
|
|
@@ -2191,6 +2963,17 @@ Raw logs return too much data for AI context windows.
|
|
|
2191
2963
|
- remove_playlist_source - Remove a source from a playlist
|
|
2192
2964
|
- reorder_playlist_sources - Reorder sources within a playlist
|
|
2193
2965
|
|
|
2966
|
+
### Template Management
|
|
2967
|
+
- update_template - Update a template's properties (name, group, tags, dimensions, colors, orientation, script)
|
|
2968
|
+
- add_template_module - Add a new module (zone) to a template
|
|
2969
|
+
- update_template_module - Update a module within a template
|
|
2970
|
+
- remove_template_module - Remove a module from a template
|
|
2971
|
+
- reorder_template_modules - Reorder modules within a template (sequence/z-order)
|
|
2972
|
+
- batch_update_template_modules - Update multiple modules in a template at once
|
|
2973
|
+
- set_module_options - Set all options for a module (replaces existing)
|
|
2974
|
+
- upsert_module_option - Add or update a single module option (matched by name)
|
|
2975
|
+
- remove_module_option - Remove a module option by name
|
|
2976
|
+
|
|
2194
2977
|
### Data Table Management
|
|
2195
2978
|
- create_data_table - Create a new data table with column schema
|
|
2196
2979
|
- update_data_table - Update table definition (partial update)
|