iobroker.mcp 0.1.4 → 0.2.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 +52 -24
- package/build/lib/devices.d.ts +127 -0
- package/build/lib/iob-uri.d.ts +20 -0
- package/build/lib/mcp-server.d.ts +125 -0
- package/{dist → build}/lib/mcp-server.js +400 -17
- package/build/lib/mcp-server.js.map +1 -0
- package/build/lib/types.d.ts +20 -0
- package/build/main.d.ts +2 -0
- package/{dist → build}/main.js +3 -0
- package/build/main.js.map +1 -0
- package/io-package.json +27 -1
- package/manifest.json +224 -6
- package/package.json +15 -3
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/main.js.map +0 -1
- /package/{dist → build}/lib/devices.js +0 -0
- /package/{dist → build}/lib/devices.js.map +0 -0
- /package/{dist → build}/lib/iob-uri.js +0 -0
- /package/{dist → build}/lib/iob-uri.js.map +0 -0
- /package/{dist → build}/lib/types.js +0 -0
- /package/{dist → build}/lib/types.js.map +0 -0
|
@@ -21,6 +21,11 @@ const AGG_MAP = {
|
|
|
21
21
|
max: 'max',
|
|
22
22
|
avg: 'average',
|
|
23
23
|
sum: 'total',
|
|
24
|
+
count: 'count',
|
|
25
|
+
minmax: 'minmax',
|
|
26
|
+
percentile: 'percentile',
|
|
27
|
+
quantile: 'quantile',
|
|
28
|
+
integral: 'integral',
|
|
24
29
|
};
|
|
25
30
|
const WEB_EXTENSION_PREFIX = 'mcp/';
|
|
26
31
|
/** Languages offered for localized names. */
|
|
@@ -59,7 +64,9 @@ class McpServer {
|
|
|
59
64
|
constructor(server, webSettings, adapter, instanceSettings, app) {
|
|
60
65
|
this.app = app;
|
|
61
66
|
this.adapter = adapter;
|
|
62
|
-
this.config = instanceSettings
|
|
67
|
+
this.config = instanceSettings
|
|
68
|
+
? instanceSettings.native
|
|
69
|
+
: adapter.config;
|
|
63
70
|
this.namespace = instanceSettings
|
|
64
71
|
? instanceSettings._id.substring('system.adapter.'.length)
|
|
65
72
|
: this.adapter.namespace;
|
|
@@ -328,8 +335,9 @@ class McpServer {
|
|
|
328
335
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: message }) }], isError: true };
|
|
329
336
|
};
|
|
330
337
|
server.registerTool('get_states', {
|
|
331
|
-
description: 'Retrieve the current value of one or multiple states'
|
|
332
|
-
|
|
338
|
+
description: 'Retrieve the current value of one or multiple states. IDs may contain wildcards, ' +
|
|
339
|
+
'e.g. "hue.0.*.brightness" expands to all matching states.',
|
|
340
|
+
inputSchema: { ids: zod_1.z.array(zod_1.z.string()).describe('Array of state IDs (wildcards "*" allowed)') },
|
|
333
341
|
}, async ({ ids }) => {
|
|
334
342
|
try {
|
|
335
343
|
return ok({ ok: true, data: { states: await this.getStates(ids) } });
|
|
@@ -357,6 +365,26 @@ class McpServer {
|
|
|
357
365
|
return fail(e);
|
|
358
366
|
}
|
|
359
367
|
});
|
|
368
|
+
server.registerTool('set_states', {
|
|
369
|
+
description: 'Set multiple states in one call (e.g. for scenes or group actions like "all lights off"). ' +
|
|
370
|
+
'Each value is coerced to its state type. Failures of single states do not abort the rest.',
|
|
371
|
+
inputSchema: {
|
|
372
|
+
states: zod_1.z
|
|
373
|
+
.array(zod_1.z.object({
|
|
374
|
+
id: zod_1.z.string().describe('State ID'),
|
|
375
|
+
value: zod_1.z.any().describe('New value (type depends on the state)'),
|
|
376
|
+
ack: zod_1.z.boolean().optional().describe('Acknowledge flag (default false)'),
|
|
377
|
+
}))
|
|
378
|
+
.describe('Array of state/value pairs to write'),
|
|
379
|
+
},
|
|
380
|
+
}, async ({ states }) => {
|
|
381
|
+
try {
|
|
382
|
+
return ok({ ok: true, data: { results: await this.setStates(states) } });
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
return fail(e);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
360
388
|
}
|
|
361
389
|
server.registerTool('get_logs', {
|
|
362
390
|
description: 'Retrieve system logs',
|
|
@@ -383,11 +411,17 @@ class McpServer {
|
|
|
383
411
|
}
|
|
384
412
|
});
|
|
385
413
|
server.registerTool('search_objects', {
|
|
386
|
-
description: 'Search objects and states by
|
|
414
|
+
description: 'Search objects and states by keyword (matched against ID and name) with optional ' +
|
|
415
|
+
'filters for object type, role, room and source adapter instance',
|
|
387
416
|
inputSchema: {
|
|
388
|
-
query: zod_1.z.string().describe('Keyword to search for'),
|
|
417
|
+
query: zod_1.z.string().describe('Keyword to search for in object IDs and names'),
|
|
418
|
+
type: zod_1.z
|
|
419
|
+
.string()
|
|
420
|
+
.optional()
|
|
421
|
+
.describe('Filter by object type, e.g. state, channel, device, enum, script, instance'),
|
|
389
422
|
role: zod_1.z.string().optional(),
|
|
390
423
|
room: zod_1.z.string().optional(),
|
|
424
|
+
adapter: zod_1.z.string().optional().describe('Filter by source adapter instance, e.g. "hue.0"'),
|
|
391
425
|
limit: zod_1.z.number().int().default(50),
|
|
392
426
|
},
|
|
393
427
|
}, async (args) => {
|
|
@@ -422,7 +456,32 @@ class McpServer {
|
|
|
422
456
|
id: zod_1.z.string(),
|
|
423
457
|
from: zod_1.z.string().optional().describe('Start time (ISO8601)'),
|
|
424
458
|
to: zod_1.z.string().optional().describe('End time (ISO8601)'),
|
|
425
|
-
agg: zod_1.z
|
|
459
|
+
agg: zod_1.z
|
|
460
|
+
.enum([
|
|
461
|
+
'raw',
|
|
462
|
+
'min',
|
|
463
|
+
'max',
|
|
464
|
+
'avg',
|
|
465
|
+
'sum',
|
|
466
|
+
'count',
|
|
467
|
+
'minmax',
|
|
468
|
+
'percentile',
|
|
469
|
+
'quantile',
|
|
470
|
+
'integral',
|
|
471
|
+
])
|
|
472
|
+
.default('raw'),
|
|
473
|
+
percentile: zod_1.z
|
|
474
|
+
.number()
|
|
475
|
+
.min(0)
|
|
476
|
+
.max(100)
|
|
477
|
+
.optional()
|
|
478
|
+
.describe('Percentile (0-100), only used with agg=percentile'),
|
|
479
|
+
quantile: zod_1.z
|
|
480
|
+
.number()
|
|
481
|
+
.min(0)
|
|
482
|
+
.max(1)
|
|
483
|
+
.optional()
|
|
484
|
+
.describe('Quantile (0-1), only used with agg=quantile'),
|
|
426
485
|
interval: zod_1.z.string().optional().describe('Aggregation interval, e.g. 15m, 1h'),
|
|
427
486
|
limit: zod_1.z.number().int().default(1000),
|
|
428
487
|
},
|
|
@@ -450,6 +509,23 @@ class McpServer {
|
|
|
450
509
|
return fail(e);
|
|
451
510
|
}
|
|
452
511
|
});
|
|
512
|
+
server.registerTool('list_adapters', {
|
|
513
|
+
description: 'List all installed adapters with metadata (version, title, description, keywords). ' +
|
|
514
|
+
'Unlike list_instances this lists what is installed, not what is running.',
|
|
515
|
+
inputSchema: {
|
|
516
|
+
language: zod_1.z
|
|
517
|
+
.enum(LANGUAGES)
|
|
518
|
+
.optional()
|
|
519
|
+
.describe('Language for title/description (defaults to the adapter language)'),
|
|
520
|
+
},
|
|
521
|
+
}, async ({ language }) => {
|
|
522
|
+
try {
|
|
523
|
+
return ok({ ok: true, data: { adapters: await this.listAdapters(language) } });
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
return fail(e);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
453
529
|
const enumInput = {
|
|
454
530
|
language: zod_1.z.enum(LANGUAGES).optional().describe('Language for names (defaults to the adapter language)'),
|
|
455
531
|
withIcons: zod_1.z.boolean().optional().describe('Include the icons of the enum and its members'),
|
|
@@ -502,6 +578,32 @@ class McpServer {
|
|
|
502
578
|
return fail(e);
|
|
503
579
|
}
|
|
504
580
|
});
|
|
581
|
+
server.registerTool('list_files', {
|
|
582
|
+
description: 'List a directory in an adapter file storage, e.g. "vis-2.0/main" or "0_userdata.0"',
|
|
583
|
+
inputSchema: { path: zod_1.z.string().describe('Path as "<adapter>[/<dir>]"') },
|
|
584
|
+
}, async ({ path }) => {
|
|
585
|
+
try {
|
|
586
|
+
return ok({ ok: true, data: { path, files: await this.listFiles(path) } });
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
return fail(e);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
server.registerTool('file_exists', {
|
|
593
|
+
description: 'Check whether a file exists in an adapter file storage',
|
|
594
|
+
inputSchema: { path: zod_1.z.string().describe('Path as "<adapter>/<dir>/<file>"') },
|
|
595
|
+
}, async ({ path }) => {
|
|
596
|
+
try {
|
|
597
|
+
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
598
|
+
const exists = await this.adapter.fileExistsAsync(adapterName, fileName, {
|
|
599
|
+
user: this.defaultUser,
|
|
600
|
+
});
|
|
601
|
+
return ok({ ok: true, data: { path, exists: !!exists } });
|
|
602
|
+
}
|
|
603
|
+
catch (e) {
|
|
604
|
+
return fail(e);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
505
607
|
// Always-on log writing (does not change states or objects).
|
|
506
608
|
server.registerTool('write_log', {
|
|
507
609
|
description: 'Write a message to the ioBroker log',
|
|
@@ -538,6 +640,73 @@ class McpServer {
|
|
|
538
640
|
return fail(e);
|
|
539
641
|
}
|
|
540
642
|
});
|
|
643
|
+
server.registerTool('delete_object', {
|
|
644
|
+
description: 'Delete an ioBroker object (and optionally all its children). ' +
|
|
645
|
+
'Be careful: deleting objects that were not created by the user can break adapters.',
|
|
646
|
+
inputSchema: {
|
|
647
|
+
id: zod_1.z.string().describe('Object ID'),
|
|
648
|
+
recursive: zod_1.z.boolean().optional().describe('Also delete all child objects'),
|
|
649
|
+
},
|
|
650
|
+
}, async ({ id, recursive }) => {
|
|
651
|
+
try {
|
|
652
|
+
await this.deleteObject(id, recursive);
|
|
653
|
+
return ok({ ok: true, data: { id, deleted: true } });
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
return fail(e);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
server.registerTool('create_state', {
|
|
660
|
+
description: 'Create a new state object (e.g. under "0_userdata.0."). Fails if the object already ' +
|
|
661
|
+
'exists - use set_object to modify existing objects.',
|
|
662
|
+
inputSchema: {
|
|
663
|
+
id: zod_1.z.string().describe('Full state ID, e.g. "0_userdata.0.myState"'),
|
|
664
|
+
name: zod_1.z.string().optional().describe('Display name (defaults to the last ID segment)'),
|
|
665
|
+
type: zod_1.z
|
|
666
|
+
.enum(['boolean', 'number', 'string', 'array', 'object', 'mixed'])
|
|
667
|
+
.default('mixed')
|
|
668
|
+
.describe('Value type of the state'),
|
|
669
|
+
role: zod_1.z.string().default('state').describe('Role, e.g. switch.light, value.temperature'),
|
|
670
|
+
read: zod_1.z.boolean().default(true),
|
|
671
|
+
write: zod_1.z.boolean().default(true),
|
|
672
|
+
unit: zod_1.z.string().optional(),
|
|
673
|
+
min: zod_1.z.number().optional(),
|
|
674
|
+
max: zod_1.z.number().optional(),
|
|
675
|
+
step: zod_1.z.number().optional(),
|
|
676
|
+
def: zod_1.z.any().optional().describe('Initial value (written with ack=true)'),
|
|
677
|
+
desc: zod_1.z.string().optional().describe('Description'),
|
|
678
|
+
},
|
|
679
|
+
}, async (args) => {
|
|
680
|
+
try {
|
|
681
|
+
return ok({ ok: true, data: await this.createState(args) });
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
return fail(e);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
server.registerTool('create_scene', {
|
|
688
|
+
description: 'Create or update a scene for the ioBroker "scenes" adapter: a named collection of ' +
|
|
689
|
+
'state/value pairs applied together (e.g. "movie night" dims lights and closes blinds). ' +
|
|
690
|
+
'Activate the scene by setting its state to true. Requires an installed scene instance.',
|
|
691
|
+
inputSchema: {
|
|
692
|
+
name: zod_1.z.string().describe('Scene name, becomes part of the ID (e.g. "movie_night")'),
|
|
693
|
+
members: zod_1.z
|
|
694
|
+
.array(zod_1.z.object({
|
|
695
|
+
id: zod_1.z.string().describe('State ID to set'),
|
|
696
|
+
value: zod_1.z.any().describe('Value applied when the scene is activated'),
|
|
697
|
+
}))
|
|
698
|
+
.describe('State/value pairs that define the scene'),
|
|
699
|
+
instance: zod_1.z.string().optional().describe('Scene adapter instance (default "scene.0")'),
|
|
700
|
+
description: zod_1.z.string().optional(),
|
|
701
|
+
},
|
|
702
|
+
}, async (args) => {
|
|
703
|
+
try {
|
|
704
|
+
return ok({ ok: true, data: await this.createScene(args) });
|
|
705
|
+
}
|
|
706
|
+
catch (e) {
|
|
707
|
+
return fail(e);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
541
710
|
server.registerTool('write_file', {
|
|
542
711
|
description: 'Write a file to an adapter file storage, e.g. "vis-2.0/main/vis-views.json"',
|
|
543
712
|
inputSchema: {
|
|
@@ -554,6 +723,51 @@ class McpServer {
|
|
|
554
723
|
return fail(e);
|
|
555
724
|
}
|
|
556
725
|
});
|
|
726
|
+
server.registerTool('delete_file', {
|
|
727
|
+
description: 'Delete a file from an adapter file storage',
|
|
728
|
+
inputSchema: { path: zod_1.z.string().describe('Path as "<adapter>/<dir>/<file>"') },
|
|
729
|
+
}, async ({ path }) => {
|
|
730
|
+
try {
|
|
731
|
+
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
732
|
+
await this.adapter.delFileAsync(adapterName, fileName, { user: this.defaultUser });
|
|
733
|
+
return ok({ ok: true, data: { path, deleted: true } });
|
|
734
|
+
}
|
|
735
|
+
catch (e) {
|
|
736
|
+
return fail(e);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
server.registerTool('rename_file', {
|
|
740
|
+
description: 'Rename or move a file/directory within the same adapter file storage, ' +
|
|
741
|
+
'e.g. "vis-2.0/main/a.json" -> "vis-2.0/backup/a.json"',
|
|
742
|
+
inputSchema: {
|
|
743
|
+
path: zod_1.z.string().describe('Current path as "<adapter>/<dir>/<file>"'),
|
|
744
|
+
new_path: zod_1.z.string().describe('New path as "<adapter>/<dir>/<file>" (same adapter)'),
|
|
745
|
+
},
|
|
746
|
+
}, async ({ path, new_path }) => {
|
|
747
|
+
try {
|
|
748
|
+
await this.renameFile(path, new_path);
|
|
749
|
+
return ok({ ok: true, data: { path: new_path } });
|
|
750
|
+
}
|
|
751
|
+
catch (e) {
|
|
752
|
+
return fail(e);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
server.registerTool('mkdir', {
|
|
756
|
+
description: 'Create a directory in an adapter file storage, e.g. "0_userdata.0/my-folder"',
|
|
757
|
+
inputSchema: { path: zod_1.z.string().describe('Path as "<adapter>/<dir>"') },
|
|
758
|
+
}, async ({ path }) => {
|
|
759
|
+
try {
|
|
760
|
+
const { adapterName, dirName } = McpServer.parseDirPath(path);
|
|
761
|
+
if (!dirName) {
|
|
762
|
+
throw new Error('Path must contain a directory after the adapter, e.g. "0_userdata.0/my-folder"');
|
|
763
|
+
}
|
|
764
|
+
await this.adapter.mkdirAsync(adapterName, dirName, { user: this.defaultUser });
|
|
765
|
+
return ok({ ok: true, data: { path } });
|
|
766
|
+
}
|
|
767
|
+
catch (e) {
|
|
768
|
+
return fail(e);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
557
771
|
}
|
|
558
772
|
// --- Resources: expose states and objects via the canonical ioBroker URI scheme ---
|
|
559
773
|
// States as `iobstate://<id>`, objects as `iobobject://<id>`. On change the server pushes a
|
|
@@ -619,20 +833,31 @@ class McpServer {
|
|
|
619
833
|
// ---------------------------------------------------------------------
|
|
620
834
|
// Tool implementations (return plain data; the wrappers serialize them)
|
|
621
835
|
// ---------------------------------------------------------------------
|
|
836
|
+
/** Build the result entry for one state. */
|
|
837
|
+
static stateEntry(id, state) {
|
|
838
|
+
if (!state) {
|
|
839
|
+
return { id, value: null, ack: false, ts: Date.now() };
|
|
840
|
+
}
|
|
841
|
+
const entry = { id, value: state.val, ack: state.ack, ts: state.ts };
|
|
842
|
+
if (state.lc !== state.ts) {
|
|
843
|
+
entry.lc = state.lc;
|
|
844
|
+
}
|
|
845
|
+
return entry;
|
|
846
|
+
}
|
|
622
847
|
async getStates(ids) {
|
|
623
848
|
const states = [];
|
|
624
849
|
for (const id of ids) {
|
|
625
850
|
try {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
851
|
+
if (id.includes('*')) {
|
|
852
|
+
// Wildcard pattern: expand to all matching states.
|
|
853
|
+
const matches = await this.adapter.getForeignStatesAsync(id, { user: this.defaultUser });
|
|
854
|
+
for (const matchId of Object.keys(matches || {})) {
|
|
855
|
+
states.push(McpServer.stateEntry(matchId, matches[matchId]));
|
|
631
856
|
}
|
|
632
|
-
states.push(entry);
|
|
633
857
|
}
|
|
634
858
|
else {
|
|
635
|
-
|
|
859
|
+
const state = await this.adapter.getForeignStateAsync(id, { user: this.defaultUser });
|
|
860
|
+
states.push(McpServer.stateEntry(id, state));
|
|
636
861
|
}
|
|
637
862
|
}
|
|
638
863
|
catch (e) {
|
|
@@ -647,6 +872,20 @@ class McpServer {
|
|
|
647
872
|
}
|
|
648
873
|
return states;
|
|
649
874
|
}
|
|
875
|
+
/** Write multiple states; failures of single states are reported per item and do not abort the rest. */
|
|
876
|
+
async setStates(items) {
|
|
877
|
+
const results = [];
|
|
878
|
+
for (const item of items) {
|
|
879
|
+
try {
|
|
880
|
+
const written = await this.setState(item.id, item.value, item.ack ?? false);
|
|
881
|
+
results.push({ id: item.id, value: written });
|
|
882
|
+
}
|
|
883
|
+
catch (e) {
|
|
884
|
+
results.push({ id: item.id, error: e instanceof Error ? e.message : String(e) });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return results;
|
|
888
|
+
}
|
|
650
889
|
getLogs(params) {
|
|
651
890
|
return new Promise((resolve, reject) => {
|
|
652
891
|
const message = { data: { size: params.limit || 200 } };
|
|
@@ -697,17 +936,22 @@ class McpServer {
|
|
|
697
936
|
};
|
|
698
937
|
}
|
|
699
938
|
async searchObjects(params) {
|
|
700
|
-
const { query = '', role, room, limit = 100 } = params;
|
|
939
|
+
const { query = '', type, role, room, adapter, limit = 100 } = params;
|
|
701
940
|
const result = await this.adapter.getObjectListAsync({
|
|
702
941
|
startkey: '',
|
|
703
942
|
endkey: '\u9999',
|
|
704
943
|
}, { sorted: true, user: this.defaultUser });
|
|
705
944
|
const allObjects = result.rows;
|
|
706
945
|
const roomMembers = room ? await this.getEnumMembers('enum.rooms.', room) : null;
|
|
946
|
+
const needle = query.toLowerCase();
|
|
707
947
|
const results = [];
|
|
708
948
|
for (const obj of allObjects) {
|
|
709
949
|
const o = obj.value;
|
|
710
|
-
|
|
950
|
+
const name = this.getName(o?.common?.name);
|
|
951
|
+
if (needle && !obj.value._id.toLowerCase().includes(needle) && !name.toLowerCase().includes(needle)) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (type && o?.type !== type) {
|
|
711
955
|
continue;
|
|
712
956
|
}
|
|
713
957
|
if (role && o?.common?.role !== role) {
|
|
@@ -716,12 +960,16 @@ class McpServer {
|
|
|
716
960
|
if (roomMembers && !roomMembers.includes(obj.value._id)) {
|
|
717
961
|
continue;
|
|
718
962
|
}
|
|
963
|
+
const sourceAdapter = obj.value._id.match(/^([^.]+\.\d+)/)?.[1] || '';
|
|
964
|
+
if (adapter && sourceAdapter !== adapter) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
719
967
|
results.push({
|
|
720
968
|
id: obj.value._id,
|
|
721
969
|
type: o?.type || 'state',
|
|
722
970
|
role: o?.common?.role || '',
|
|
723
|
-
name
|
|
724
|
-
adapter:
|
|
971
|
+
name,
|
|
972
|
+
adapter: sourceAdapter,
|
|
725
973
|
});
|
|
726
974
|
if (results.length >= limit) {
|
|
727
975
|
break;
|
|
@@ -750,6 +998,12 @@ class McpServer {
|
|
|
750
998
|
if (params.to) {
|
|
751
999
|
options.end = new Date(params.to).getTime();
|
|
752
1000
|
}
|
|
1001
|
+
if (params.percentile !== undefined) {
|
|
1002
|
+
options.percentile = params.percentile;
|
|
1003
|
+
}
|
|
1004
|
+
if (params.quantile !== undefined) {
|
|
1005
|
+
options.quantile = params.quantile;
|
|
1006
|
+
}
|
|
753
1007
|
const step = params.interval ? this.parseInterval(params.interval) : undefined;
|
|
754
1008
|
if (step) {
|
|
755
1009
|
options.step = step;
|
|
@@ -781,6 +1035,22 @@ class McpServer {
|
|
|
781
1035
|
}
|
|
782
1036
|
return result;
|
|
783
1037
|
}
|
|
1038
|
+
/** List installed adapters (system.adapter.<name> objects, no instances). */
|
|
1039
|
+
async listAdapters(language) {
|
|
1040
|
+
const lang = language || this.language;
|
|
1041
|
+
const view = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.香' }, { user: this.defaultUser });
|
|
1042
|
+
return view.rows.map(row => {
|
|
1043
|
+
const common = (row.value?.common || {});
|
|
1044
|
+
return {
|
|
1045
|
+
name: common.name || row.id.replace('system.adapter.', ''),
|
|
1046
|
+
version: common.version || '',
|
|
1047
|
+
title: getText(common.titleLang || common.title, lang),
|
|
1048
|
+
description: getText(common.desc, lang),
|
|
1049
|
+
keywords: common.keywords || [],
|
|
1050
|
+
mode: common.mode || '',
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
784
1054
|
async listHosts() {
|
|
785
1055
|
const objs = await this.adapter.getForeignObjectsAsync('system.host.*', 'host', { user: this.defaultUser });
|
|
786
1056
|
const result = [];
|
|
@@ -918,6 +1188,26 @@ class McpServer {
|
|
|
918
1188
|
const data = base64 ? Buffer.from(content, 'base64') : content;
|
|
919
1189
|
await this.adapter.writeFileAsync(adapterName, fileName, data, { user: this.defaultUser });
|
|
920
1190
|
}
|
|
1191
|
+
/** List a directory in an adapter file storage. */
|
|
1192
|
+
async listFiles(path) {
|
|
1193
|
+
const { adapterName, dirName } = McpServer.parseDirPath(path);
|
|
1194
|
+
const entries = await this.adapter.readDirAsync(adapterName, dirName, { user: this.defaultUser });
|
|
1195
|
+
return (entries || []).map(entry => ({
|
|
1196
|
+
file: entry.file,
|
|
1197
|
+
isDir: !!entry.isDir,
|
|
1198
|
+
size: entry.stats?.size,
|
|
1199
|
+
modified: entry.modifiedAt,
|
|
1200
|
+
}));
|
|
1201
|
+
}
|
|
1202
|
+
/** Rename/move a file within the same adapter file storage. */
|
|
1203
|
+
async renameFile(path, newPath) {
|
|
1204
|
+
const from = McpServer.parseFilePath(path);
|
|
1205
|
+
const to = McpServer.parseFilePath(newPath);
|
|
1206
|
+
if (from.adapterName !== to.adapterName) {
|
|
1207
|
+
throw new Error('Renaming across adapter storages is not supported; both paths must share the adapter');
|
|
1208
|
+
}
|
|
1209
|
+
await this.adapter.renameAsync(from.adapterName, from.fileName, to.fileName, { user: this.defaultUser });
|
|
1210
|
+
}
|
|
921
1211
|
/** Create or update an object, merging common/native into an existing object (n8n `setIobObject`). */
|
|
922
1212
|
async setObject(id, obj) {
|
|
923
1213
|
let existing = await this.adapter.getForeignObjectAsync(id, { user: this.defaultUser });
|
|
@@ -946,6 +1236,99 @@ class McpServer {
|
|
|
946
1236
|
}
|
|
947
1237
|
return { adapterName, fileName };
|
|
948
1238
|
}
|
|
1239
|
+
/** Split a directory path "<adapter>[/<dir>]" into adapter name and (possibly empty) directory. */
|
|
1240
|
+
static parseDirPath(path) {
|
|
1241
|
+
const [adapterName, ...rest] = path.replace(/^\//, '').replace(/\/$/, '').split('/');
|
|
1242
|
+
if (!adapterName) {
|
|
1243
|
+
throw new Error('Path must start with an adapter name, e.g. "vis-2.0/main" or "0_userdata.0"');
|
|
1244
|
+
}
|
|
1245
|
+
return { adapterName, dirName: rest.join('/') };
|
|
1246
|
+
}
|
|
1247
|
+
/** Delete an object, optionally with all its children. */
|
|
1248
|
+
async deleteObject(id, recursive) {
|
|
1249
|
+
const existing = await this.adapter.getForeignObjectAsync(id, { user: this.defaultUser });
|
|
1250
|
+
if (!existing) {
|
|
1251
|
+
throw new Error(`Object "${id}" does not exist`);
|
|
1252
|
+
}
|
|
1253
|
+
await this.adapter.delForeignObjectAsync(id, { user: this.defaultUser, recursive: !!recursive });
|
|
1254
|
+
}
|
|
1255
|
+
/** Create a new state object; refuses to overwrite an existing object. */
|
|
1256
|
+
async createState(params) {
|
|
1257
|
+
const existing = await this.adapter.getForeignObjectAsync(params.id, { user: this.defaultUser });
|
|
1258
|
+
if (existing) {
|
|
1259
|
+
throw new Error(`Object "${params.id}" already exists - use set_object to modify it`);
|
|
1260
|
+
}
|
|
1261
|
+
const common = {
|
|
1262
|
+
name: params.name || params.id.split('.').pop() || params.id,
|
|
1263
|
+
type: params.type || 'mixed',
|
|
1264
|
+
role: params.role || 'state',
|
|
1265
|
+
read: params.read !== false,
|
|
1266
|
+
write: params.write !== false,
|
|
1267
|
+
};
|
|
1268
|
+
if (params.unit !== undefined) {
|
|
1269
|
+
common.unit = params.unit;
|
|
1270
|
+
}
|
|
1271
|
+
if (params.min !== undefined) {
|
|
1272
|
+
common.min = params.min;
|
|
1273
|
+
}
|
|
1274
|
+
if (params.max !== undefined) {
|
|
1275
|
+
common.max = params.max;
|
|
1276
|
+
}
|
|
1277
|
+
if (params.step !== undefined) {
|
|
1278
|
+
common.step = params.step;
|
|
1279
|
+
}
|
|
1280
|
+
if (params.desc !== undefined) {
|
|
1281
|
+
common.desc = params.desc;
|
|
1282
|
+
}
|
|
1283
|
+
await this.adapter.setForeignObjectAsync(params.id, { type: 'state', common, native: {} }, { user: this.defaultUser });
|
|
1284
|
+
if (params.def !== undefined) {
|
|
1285
|
+
const coerced = McpServer.coerceValue(params.def, common.type);
|
|
1286
|
+
await this.adapter.setForeignStateAsync(params.id, coerced, true, { user: this.defaultUser });
|
|
1287
|
+
}
|
|
1288
|
+
return { id: params.id };
|
|
1289
|
+
}
|
|
1290
|
+
/** Create or update a scene object for the ioBroker "scenes" adapter. */
|
|
1291
|
+
async createScene(params) {
|
|
1292
|
+
const instance = params.instance || 'scene.0';
|
|
1293
|
+
const instanceObj = await this.adapter.getForeignObjectAsync(`system.adapter.${instance}`, {
|
|
1294
|
+
user: this.defaultUser,
|
|
1295
|
+
});
|
|
1296
|
+
if (!instanceObj) {
|
|
1297
|
+
throw new Error(`Scene adapter instance "${instance}" is not installed. Install the ioBroker "scenes" adapter first.`);
|
|
1298
|
+
}
|
|
1299
|
+
// Scene IDs may not contain the characters that are forbidden in ioBroker IDs.
|
|
1300
|
+
const sceneName = params.name.replace(/[\s.*?"'[\]]/g, '_');
|
|
1301
|
+
const id = `${instance}.${sceneName}`;
|
|
1302
|
+
const sceneObj = {
|
|
1303
|
+
type: 'state',
|
|
1304
|
+
common: {
|
|
1305
|
+
name: params.name,
|
|
1306
|
+
type: 'boolean',
|
|
1307
|
+
role: 'scene.state',
|
|
1308
|
+
desc: params.description || '',
|
|
1309
|
+
read: true,
|
|
1310
|
+
write: true,
|
|
1311
|
+
def: false,
|
|
1312
|
+
engine: `system.adapter.${instance}`,
|
|
1313
|
+
enabled: true,
|
|
1314
|
+
},
|
|
1315
|
+
native: {
|
|
1316
|
+
onTrue: { trigger: {}, cron: null, astro: null },
|
|
1317
|
+
onFalse: { enabled: false, trigger: {}, cron: null, astro: null },
|
|
1318
|
+
members: params.members.map(member => ({
|
|
1319
|
+
id: member.id,
|
|
1320
|
+
setIfTrue: member.value,
|
|
1321
|
+
setIfFalse: null,
|
|
1322
|
+
stopAllDelays: true,
|
|
1323
|
+
delay: 0,
|
|
1324
|
+
disabled: false,
|
|
1325
|
+
})),
|
|
1326
|
+
burstInterval: 0,
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
await this.adapter.setForeignObjectAsync(id, sceneObj, { user: this.defaultUser });
|
|
1330
|
+
return { id, members: params.members.length };
|
|
1331
|
+
}
|
|
949
1332
|
// --- helpers ---
|
|
950
1333
|
/** Resolve the member ids of a room/function enum matched by id or (localized) name. */
|
|
951
1334
|
async getEnumMembers(prefix, nameOrId) {
|