@thinger-io/cli-core 0.1.0 → 0.2.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/CHANGELOG.md +100 -0
- package/README.md +37 -5
- package/lib/api.js +3 -0
- package/lib/auth.js +29 -9
- package/lib/bucket.js +3 -0
- package/lib/cli.js +15 -2
- package/lib/config.js +55 -5
- package/lib/constants.js +8 -0
- package/lib/dashboard.js +605 -0
- package/lib/docs.js +182 -0
- package/lib/errors.js +29 -4
- package/lib/mcp/catalog.js +1235 -0
- package/lib/mcp/helpers.js +1 -1
- package/lib/mcp/registry.js +72 -30
- package/lib/mcp/resource-hooks.js +145 -0
- package/lib/mcp/server.js +46 -5
- package/lib/mcp/tools-buckets.js +79 -80
- package/lib/mcp/tools-dashboards.js +123 -0
- package/lib/mcp/tools-devices.js +88 -234
- package/lib/mcp/tools-misc.js +48 -0
- package/lib/mcp/tools-plugins.js +63 -0
- package/lib/mcp/tools-products.js +32 -385
- package/lib/mcp/tools-resource.js +760 -0
- package/lib/mcp/tools-storage.js +173 -0
- package/lib/openapi.js +169 -0
- package/lib/paths.js +8 -0
- package/lib/resource.js +2 -0
- package/lib/storage.js +2 -1
- package/openapi/swagger.json +1 -0
- package/package.json +3 -2
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -84
- package/lib/mcp/tools-resources.js +0 -83
- package/lib/mcp/tools-tokens.js +0 -370
|
@@ -1,405 +1,52 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
productExists,
|
|
7
|
-
updateProduct,
|
|
8
|
-
deleteProduct,
|
|
9
|
-
getProductApi,
|
|
10
|
-
getProductProperties,
|
|
11
|
-
getProductProperty,
|
|
12
|
-
setProductProperty,
|
|
13
|
-
deleteProductProperty,
|
|
14
|
-
} from '../product.js';
|
|
15
|
-
import { alreadyExistsError, inputError } from '../errors.js';
|
|
2
|
+
import api from '../api.js';
|
|
3
|
+
import { TIMEOUTS } from '../constants.js';
|
|
4
|
+
import { apiError, inputError } from '../errors.js';
|
|
5
|
+
import { v1 } from '../paths.js';
|
|
16
6
|
|
|
17
|
-
const
|
|
7
|
+
const json = (data) => ({
|
|
8
|
+
content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }],
|
|
9
|
+
isError: false,
|
|
10
|
+
});
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
function iconSpecFrom(icon) {
|
|
21
|
-
if (!icon.source) throw inputError('icon.source is required when icon is set');
|
|
22
|
-
const type = icon.type || 'fa';
|
|
23
|
-
if (type !== 'fa' && type !== 'svg') {
|
|
24
|
-
throw inputError(`icon.type must be "fa" or "svg" (got "${type}")`);
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
type,
|
|
28
|
-
source: icon.source,
|
|
29
|
-
color: icon.color || '#000000',
|
|
30
|
-
background: icon.background || '#ffffff',
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function toolProducts(args) {
|
|
35
|
-
const user = args.user;
|
|
36
|
-
const products = await getProducts(user);
|
|
37
|
-
if (products.length === 0) {
|
|
38
|
-
return { content: [{ type: 'text', text: 'No products configured' }], isError: false };
|
|
39
|
-
}
|
|
40
|
-
const lines = await Promise.all(
|
|
41
|
-
products.map(async (p) => {
|
|
42
|
-
const id = p.product;
|
|
43
|
-
const label = p.name && p.name !== id ? ` (${p.name})` : '';
|
|
44
|
-
const enabled = p.enabled ? '✓' : '✗';
|
|
45
|
-
const apis = await getProductApi(id, user);
|
|
46
|
-
const scripts = Object.keys(apis).length;
|
|
47
|
-
const scriptsLabel = scripts > 0 ? ` — ${scripts} API resource(s)` : '';
|
|
48
|
-
return `${enabled} ${id}${label}${scriptsLabel}`;
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
return {
|
|
52
|
-
content: [{ type: 'text', text: `${products.length} product(s):\n${lines.join('\n')}` }],
|
|
53
|
-
isError: false,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function toolProductCreate(args) {
|
|
12
|
+
function productPath(args) {
|
|
58
13
|
if (!args.product) throw inputError('product is required');
|
|
59
|
-
|
|
60
|
-
throw inputError(
|
|
61
|
-
`Invalid product id "${args.product}" (allowed: letters, digits, dot, dash, underscore)`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if (await productExists(args.product, args.user)) {
|
|
65
|
-
throw alreadyExistsError(`Product "${args.product}" already exists`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const config = args.config ? { ...args.config } : {};
|
|
69
|
-
if (args.icon) config.icons = [{ conditions: [], icon: iconSpecFrom(args.icon) }];
|
|
70
|
-
|
|
71
|
-
await createProduct(args.product, args.user, {
|
|
72
|
-
name: args.name,
|
|
73
|
-
description: args.description,
|
|
74
|
-
enabled: args.enabled ?? true,
|
|
75
|
-
config,
|
|
76
|
-
profile: args.profile,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
content: [
|
|
81
|
-
{
|
|
82
|
-
type: 'text',
|
|
83
|
-
text: `Created product "${args.product}"${args.name ? ` (${args.name})` : ''}.`,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
isError: false,
|
|
87
|
-
};
|
|
14
|
+
return `${v1(args.user)}/products/${encodeURIComponent(args.product)}`;
|
|
88
15
|
}
|
|
89
16
|
|
|
90
|
-
async function
|
|
91
|
-
if (!args.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const current = await getProduct(args.product, args.user);
|
|
104
|
-
const config = { ...(current?.config || {}) };
|
|
105
|
-
config.icons = [
|
|
106
|
-
{
|
|
107
|
-
conditions: [],
|
|
108
|
-
icon,
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
patch.config = config;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const fields = Object.keys(patch);
|
|
115
|
-
if (fields.length === 0) {
|
|
116
|
-
throw inputError('at least one of name, description, enabled, icon must be provided');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
await updateProduct(args.product, args.user, patch);
|
|
120
|
-
|
|
121
|
-
const summary = fields.map((f) => (f === 'config' ? 'icon' : f)).join(', ');
|
|
122
|
-
return {
|
|
123
|
-
content: [{ type: 'text', text: `Updated product "${args.product}" (${summary}).` }],
|
|
124
|
-
isError: false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function toolProductDelete(args) {
|
|
129
|
-
if (!args.product) throw inputError('product is required');
|
|
130
|
-
const removed = await deleteProduct(args.product, args.user);
|
|
131
|
-
return {
|
|
132
|
-
content: [
|
|
133
|
-
{
|
|
134
|
-
type: 'text',
|
|
135
|
-
text: removed
|
|
136
|
-
? `Deleted product "${args.product}".`
|
|
137
|
-
: `Product "${args.product}" did not exist.`,
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
isError: false,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function toolProductPropertyGet(args) {
|
|
145
|
-
if (!args.product) throw inputError('product is required');
|
|
146
|
-
if (args.property) {
|
|
147
|
-
const value = await getProductProperty(args.product, args.property, args.user);
|
|
148
|
-
return {
|
|
149
|
-
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
150
|
-
isError: false,
|
|
151
|
-
};
|
|
17
|
+
async function toolResourceCall(args) {
|
|
18
|
+
if (!args.resource) throw inputError('resource is required');
|
|
19
|
+
try {
|
|
20
|
+
const res = await api.post(
|
|
21
|
+
`${productPath(args)}/resources/${encodeURIComponent(args.resource)}`,
|
|
22
|
+
args.data ?? {},
|
|
23
|
+
{ timeout: TIMEOUTS.API_LONG_MS },
|
|
24
|
+
);
|
|
25
|
+
return json(res.data);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw apiError(e, {
|
|
28
|
+
notFound: `Not found: product ${args.product} or its resource ${args.resource}`,
|
|
29
|
+
});
|
|
152
30
|
}
|
|
153
|
-
const list = await getProductProperties(args.product, args.user);
|
|
154
|
-
const names = Array.isArray(list) ? list.map((p) => p.property).filter(Boolean) : [];
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: 'text', text: names.length ? names.join('\n') : '(no properties)' }],
|
|
157
|
-
isError: false,
|
|
158
|
-
};
|
|
159
31
|
}
|
|
160
32
|
|
|
161
|
-
|
|
162
|
-
if (!args.product) throw inputError('product is required');
|
|
163
|
-
if (!args.property) throw inputError('property is required');
|
|
164
|
-
if (args.value === undefined) throw inputError('value is required');
|
|
165
|
-
const saved = await setProductProperty(args.product, args.property, args.value, args.user);
|
|
166
|
-
return {
|
|
167
|
-
content: [
|
|
168
|
-
{
|
|
169
|
-
type: 'text',
|
|
170
|
-
text: `Saved property "${saved.property}" on product "${saved.product || args.product}" (value: ${JSON.stringify(saved.value)})`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
isError: false,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function toolProductPropertyDelete(args) {
|
|
178
|
-
if (!args.product) throw inputError('product is required');
|
|
179
|
-
if (!args.property) throw inputError('property is required');
|
|
180
|
-
const removed = await deleteProductProperty(args.product, args.property, args.user);
|
|
181
|
-
return {
|
|
182
|
-
content: [
|
|
183
|
-
{
|
|
184
|
-
type: 'text',
|
|
185
|
-
text: removed
|
|
186
|
-
? `Deleted property "${args.property}" from product "${args.product}".`
|
|
187
|
-
: `Property "${args.property}" was not set on product "${args.product}".`,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
isError: false,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
33
|
+
const userParam = { type: 'string', description: 'Account to act on (admin only).' };
|
|
193
34
|
|
|
194
35
|
export const tools = [
|
|
195
36
|
{
|
|
196
|
-
name: '
|
|
197
|
-
description:
|
|
198
|
-
'List all products configured on the account, with their enabled flag and how many API resources their profile declares. Use this before assigning a device to a product with {tool:device_set_product}, or before editing a product profile with the {family:product_profile} tools.',
|
|
199
|
-
inputSchema: {
|
|
200
|
-
type: 'object',
|
|
201
|
-
properties: {
|
|
202
|
-
user: { type: 'string', description: 'API user' },
|
|
203
|
-
},
|
|
204
|
-
required: [],
|
|
205
|
-
},
|
|
206
|
-
handler: toolProducts,
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
name: 'product_create',
|
|
210
|
-
description: `Create a new product. The product ID is immutable — pick it carefully (letters, digits, dot, dash, underscore). Optionally set the display name, description and icon at creation time.
|
|
211
|
-
|
|
212
|
-
A product can be created bare and configured afterwards with the profile tools ({tool:product_profile_bucket_set}, {tool:product_profile_api_set}, {tool:product_profile_property_set}), or in one call by passing the whole \`profile\` object (and \`config\`) as the server stores them.
|
|
213
|
-
|
|
214
|
-
Failure mode: refuses to overwrite an existing product. Use {tool:product_update} to change metadata on a product that already exists.
|
|
215
|
-
|
|
216
|
-
Typical flow: {tool:products} to discover what is already there, {tool:product_create} to register the new one, the profile tools to declare its buckets / API resources / property handlers, {tool:device_set_product} to assign devices.`,
|
|
217
|
-
inputSchema: {
|
|
218
|
-
type: 'object',
|
|
219
|
-
properties: {
|
|
220
|
-
product: {
|
|
221
|
-
type: 'string',
|
|
222
|
-
description:
|
|
223
|
-
'Stable product ID (letters, digits, dot, dash, underscore). Cannot be changed later.',
|
|
224
|
-
},
|
|
225
|
-
name: {
|
|
226
|
-
type: 'string',
|
|
227
|
-
description:
|
|
228
|
-
'Display name shown across the platform UI. Defaults to the product ID.',
|
|
229
|
-
},
|
|
230
|
-
description: {
|
|
231
|
-
type: 'string',
|
|
232
|
-
description: 'Free-form description.',
|
|
233
|
-
},
|
|
234
|
-
enabled: {
|
|
235
|
-
type: 'boolean',
|
|
236
|
-
description: 'Whether the product starts enabled. Default: true.',
|
|
237
|
-
},
|
|
238
|
-
icon: {
|
|
239
|
-
type: 'object',
|
|
240
|
-
description:
|
|
241
|
-
"Optional icon. Pick `type: 'fa'` for a Font Awesome class or `type: 'svg'` for inline SVG markup. Defaults to the platform's generic product icon.",
|
|
242
|
-
properties: {
|
|
243
|
-
type: {
|
|
244
|
-
type: 'string',
|
|
245
|
-
enum: ['fa', 'svg'],
|
|
246
|
-
description:
|
|
247
|
-
"Icon kind: 'fa' (default) for Font Awesome class, 'svg' for inline SVG markup.",
|
|
248
|
-
},
|
|
249
|
-
source: {
|
|
250
|
-
type: 'string',
|
|
251
|
-
description:
|
|
252
|
-
'Font Awesome class (e.g. "fab fa-linux", "fas fa-server") or full SVG markup including the `<svg>` wrapper.',
|
|
253
|
-
},
|
|
254
|
-
color: {
|
|
255
|
-
type: 'string',
|
|
256
|
-
description: 'Hex foreground color. Default: #000000.',
|
|
257
|
-
},
|
|
258
|
-
background: {
|
|
259
|
-
type: 'string',
|
|
260
|
-
description: 'Hex background color. Default: #ffffff.',
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
required: ['source'],
|
|
264
|
-
},
|
|
265
|
-
config: {
|
|
266
|
-
type: 'object',
|
|
267
|
-
description:
|
|
268
|
-
'Raw product `config` as the server stores it (e.g. autoprovisioning rules). `icon` is merged into it.',
|
|
269
|
-
},
|
|
270
|
-
profile: {
|
|
271
|
-
type: 'object',
|
|
272
|
-
description:
|
|
273
|
-
'Raw product `profile` as the server stores it: `{ api, buckets, properties, ... }`. Prefer the profile tools for incremental changes.',
|
|
274
|
-
},
|
|
275
|
-
user: { type: 'string', description: 'API user' },
|
|
276
|
-
},
|
|
277
|
-
required: ['product'],
|
|
278
|
-
},
|
|
279
|
-
handler: toolProductCreate,
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
name: 'product_update',
|
|
283
|
-
description: `Update product metadata in place. Pass any subset of fields (\`name\`, \`description\`, \`enabled\`, \`icon\`) — only the supplied ones are modified, the rest are left untouched. Useful to set a friendlier display name shown across the platform UI, change the description, toggle the product on or off, or replace its icon.
|
|
284
|
-
|
|
285
|
-
Icons accept Font Awesome classes (\`{ type: "fa", source: "fab fa-linux" }\`) or inline SVG markup (\`{ type: "svg", source: "<svg ...>...</svg>" }\`). When an icon is supplied, the current product config is fetched first so unrelated fields are preserved.
|
|
286
|
-
|
|
287
|
-
Does not change the product ID. To rename the ID itself, the product has to be recreated and devices reassigned.`,
|
|
288
|
-
inputSchema: {
|
|
289
|
-
type: 'object',
|
|
290
|
-
properties: {
|
|
291
|
-
product: { type: 'string', description: 'Product ID to update.' },
|
|
292
|
-
name: {
|
|
293
|
-
type: 'string',
|
|
294
|
-
description:
|
|
295
|
-
'Display name shown across the platform UI (dashboards, lists, etc.).',
|
|
296
|
-
},
|
|
297
|
-
description: {
|
|
298
|
-
type: 'string',
|
|
299
|
-
description: 'Free-form description.',
|
|
300
|
-
},
|
|
301
|
-
enabled: {
|
|
302
|
-
type: 'boolean',
|
|
303
|
-
description:
|
|
304
|
-
'Enable or disable the product. Disabled products do not apply their profile to devices.',
|
|
305
|
-
},
|
|
306
|
-
icon: {
|
|
307
|
-
type: 'object',
|
|
308
|
-
description:
|
|
309
|
-
"Replace the product's icon. Pick `type: 'fa'` for a Font Awesome class or `type: 'svg'` for inline SVG markup.",
|
|
310
|
-
properties: {
|
|
311
|
-
type: {
|
|
312
|
-
type: 'string',
|
|
313
|
-
enum: ['fa', 'svg'],
|
|
314
|
-
description:
|
|
315
|
-
"Icon kind: 'fa' (default) for Font Awesome class, 'svg' for inline SVG markup.",
|
|
316
|
-
},
|
|
317
|
-
source: {
|
|
318
|
-
type: 'string',
|
|
319
|
-
description:
|
|
320
|
-
'Font Awesome class (e.g. "fab fa-linux", "fas fa-server") or full SVG markup including the `<svg>` wrapper.',
|
|
321
|
-
},
|
|
322
|
-
color: {
|
|
323
|
-
type: 'string',
|
|
324
|
-
description: 'Hex foreground color. Default: #000000.',
|
|
325
|
-
},
|
|
326
|
-
background: {
|
|
327
|
-
type: 'string',
|
|
328
|
-
description: 'Hex background color. Default: #ffffff.',
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
required: ['source'],
|
|
332
|
-
},
|
|
333
|
-
user: { type: 'string', description: 'API user' },
|
|
334
|
-
},
|
|
335
|
-
required: ['product'],
|
|
336
|
-
},
|
|
337
|
-
handler: toolProductUpdate,
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
name: 'product_delete',
|
|
37
|
+
name: 'product_resource_call',
|
|
341
38
|
description:
|
|
342
|
-
|
|
343
|
-
inputSchema: {
|
|
344
|
-
type: 'object',
|
|
345
|
-
properties: {
|
|
346
|
-
product: { type: 'string', description: 'Product ID to delete.' },
|
|
347
|
-
user: { type: 'string', description: 'API user' },
|
|
348
|
-
},
|
|
349
|
-
required: ['product'],
|
|
350
|
-
},
|
|
351
|
-
handler: toolProductDelete,
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
name: 'product_property_get',
|
|
355
|
-
description: `Read a product-level property (not a device property). Product properties are structured JSON values attached to the product itself — typical uses include dashboard configuration or shared feature flags. Call without \`property\` to list the names of every property configured on the product.`,
|
|
356
|
-
inputSchema: {
|
|
357
|
-
type: 'object',
|
|
358
|
-
properties: {
|
|
359
|
-
product: { type: 'string', description: 'Product ID.' },
|
|
360
|
-
property: {
|
|
361
|
-
type: 'string',
|
|
362
|
-
description:
|
|
363
|
-
'Property name. Omit to list the names of all properties on the product.',
|
|
364
|
-
},
|
|
365
|
-
user: { type: 'string', description: 'API user' },
|
|
366
|
-
},
|
|
367
|
-
required: ['product'],
|
|
368
|
-
},
|
|
369
|
-
handler: toolProductPropertyGet,
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
name: 'product_property_set',
|
|
373
|
-
description: `Upsert a product-level property: creates the property if missing, overwrites it wholesale if it already exists. The value can be any JSON-serialisable shape (string, number, object, array).
|
|
374
|
-
|
|
375
|
-
Naming convention: this tool follows the upsert pattern shared by every per-product leaf (\`*_set\`, \`*_write\`). Container-level operations (the product itself) split create and update into separate tools so a fresh registration cannot accidentally overwrite an existing product's metadata.`,
|
|
376
|
-
inputSchema: {
|
|
377
|
-
type: 'object',
|
|
378
|
-
properties: {
|
|
379
|
-
product: { type: 'string', description: 'Product ID.' },
|
|
380
|
-
property: { type: 'string', description: 'Property name to create or overwrite.' },
|
|
381
|
-
value: {
|
|
382
|
-
description:
|
|
383
|
-
'Any JSON-serialisable value (string, number, boolean, object, array).',
|
|
384
|
-
},
|
|
385
|
-
user: { type: 'string', description: 'API user' },
|
|
386
|
-
},
|
|
387
|
-
required: ['product', 'property', 'value'],
|
|
388
|
-
},
|
|
389
|
-
handler: toolProductPropertySet,
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
name: 'product_property_delete',
|
|
393
|
-
description: `Delete a product-level property. Idempotent — returns success even if the property was not set.`,
|
|
39
|
+
"Call a product API resource for the product as a whole, as an external system feeding data in would. The entry needs a `device_id_resolver` (a function of the product code that returns each payload's device); `data` may be an array, one payload per device. To call it for one device, use {tool:device_resource_call}.",
|
|
394
40
|
inputSchema: {
|
|
395
41
|
type: 'object',
|
|
396
42
|
properties: {
|
|
397
|
-
product: { type: 'string', description: 'Product
|
|
398
|
-
|
|
399
|
-
|
|
43
|
+
product: { type: 'string', description: 'Product id.' },
|
|
44
|
+
resource: { type: 'string', description: 'Resource name in the product profile.' },
|
|
45
|
+
data: { description: 'JSON payload.' },
|
|
46
|
+
user: userParam,
|
|
400
47
|
},
|
|
401
|
-
required: ['product', '
|
|
48
|
+
required: ['product', 'resource'],
|
|
402
49
|
},
|
|
403
|
-
handler:
|
|
50
|
+
handler: toolResourceCall,
|
|
404
51
|
},
|
|
405
52
|
];
|