@iobroker/mcp-server 1.5.0 → 1.6.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/README.md +14 -4
- package/build/devices.d.ts +1 -1
- package/build/devices.js +1 -1
- package/build/index.d.ts +31 -4
- package/build/index.js +176 -97
- package/build/index.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createInProcessMcp = exports.McpServer = void 0;
|
|
7
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
7
8
|
const node_crypto_1 = require("node:crypto");
|
|
8
9
|
const node_os_1 = __importDefault(require("node:os"));
|
|
9
10
|
const node_net_1 = __importDefault(require("node:net"));
|
|
@@ -173,8 +174,10 @@ class McpServer {
|
|
|
173
174
|
config;
|
|
174
175
|
extension;
|
|
175
176
|
routerPrefix;
|
|
176
|
-
/** The ioBroker user whose permissions
|
|
177
|
+
/** The ioBroker user whose permissions MCP requests run with when no user is logged in. */
|
|
177
178
|
defaultUser;
|
|
179
|
+
/** The user of the MCP request being processed, set by {@link runAs} for each HTTP request. */
|
|
180
|
+
requestUser = new node_async_hooks_1.AsyncLocalStorage();
|
|
178
181
|
/** Default language used to localize device/room/function names. */
|
|
179
182
|
language;
|
|
180
183
|
/**
|
|
@@ -227,11 +230,13 @@ class McpServer {
|
|
|
227
230
|
// (e.g. `/mcp/`) within the shared web server, and strip the trailing slash so the route
|
|
228
231
|
// matches both `/mcp` and `/mcp/` (Express, non-strict).
|
|
229
232
|
this.mcpPath = this.extension ? this.routerPrefix.replace(/\/$/, '') : '/mcp';
|
|
230
|
-
// Determine the ioBroker user whose permissions
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
const rawUser = this.
|
|
234
|
-
|
|
233
|
+
// Determine the ioBroker user whose permissions unauthenticated MCP requests run with.
|
|
234
|
+
// As a web extension the host `web` instance decides about authentication, so its default
|
|
235
|
+
// user wins there; otherwise prefer this adapter's own setting. Finally fall back to "admin".
|
|
236
|
+
const rawUser = (this.extension
|
|
237
|
+
? webSettings.defaultUser || this.config.defaultUser
|
|
238
|
+
: this.config.defaultUser || webSettings.defaultUser) || 'admin';
|
|
239
|
+
this.defaultUser = McpServer.toUserId(rawUser);
|
|
235
240
|
this.config.defaultUser = this.defaultUser;
|
|
236
241
|
this.language = webSettings.language || 'en';
|
|
237
242
|
// Embedded callers may set the permission toggles explicitly (they don't read the mcp config).
|
|
@@ -267,6 +272,9 @@ class McpServer {
|
|
|
267
272
|
this.linksUnavailableReason = '';
|
|
268
273
|
}
|
|
269
274
|
this.linksAvailable = !this.linksUnavailableReason;
|
|
275
|
+
if (this.linksAvailable) {
|
|
276
|
+
this.removeStaleTransferDirs();
|
|
277
|
+
}
|
|
270
278
|
// State writes are declared destructive unless turned off explicitly
|
|
271
279
|
this.config.setStateDestructive = this.config.setStateDestructive !== false;
|
|
272
280
|
// Authentication is enforced by us only when we run standalone (we own the Express app) and
|
|
@@ -604,15 +612,57 @@ class McpServer {
|
|
|
604
612
|
pro: false,
|
|
605
613
|
};
|
|
606
614
|
}
|
|
615
|
+
/** Normalize a plain user name or a `system.user.*` id to the full id. */
|
|
616
|
+
static toUserId(user) {
|
|
617
|
+
return (user.startsWith('system.user.') ? user : `system.user.${user}`);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* The ioBroker user a request runs as: the logged-in user when the request was authenticated (by
|
|
621
|
+
* us or, as a web extension, by the host `web` instance), otherwise the default user.
|
|
622
|
+
*/
|
|
623
|
+
getRequestUser(req) {
|
|
624
|
+
const user = req.user;
|
|
625
|
+
return user ? McpServer.toUserId(user) : this.defaultUser;
|
|
626
|
+
}
|
|
627
|
+
/** The user the current tool call runs as. Outside a request (embedded mode) the default user. */
|
|
628
|
+
get user() {
|
|
629
|
+
return this.requestUser.getStore() || this.defaultUser;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Process a request of a session with the permissions of the session's user. The SDK dispatches the
|
|
633
|
+
* contained messages within this call, so every tool reached from here sees that user in {@link user}.
|
|
634
|
+
*/
|
|
635
|
+
runAs(user, handle) {
|
|
636
|
+
return this.requestUser.run(user, handle);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get the session of a request. A session is bound to the user that opened it: requests of another
|
|
640
|
+
* user are answered like an unknown session, so that a leaked session id gives no access.
|
|
641
|
+
*/
|
|
642
|
+
getSession(req, res, sessionId) {
|
|
643
|
+
const session = this.sessions[sessionId];
|
|
644
|
+
if (session && session.user !== this.getRequestUser(req)) {
|
|
645
|
+
this.adapter.log.warn(`Rejected MCP request from ${req.ip}: session ${sessionId} belongs to another user than ${this.getRequestUser(req)}`);
|
|
646
|
+
this.sendSessionNotFound(res, sessionId);
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
if (!session) {
|
|
650
|
+
this.sendSessionNotFound(res, sessionId);
|
|
651
|
+
}
|
|
652
|
+
return session;
|
|
653
|
+
}
|
|
607
654
|
/** Handle client -> server messages (initialize, tools/call, ...). */
|
|
608
655
|
async handleMcpPost(req, res) {
|
|
609
656
|
const sessionId = req.headers['mcp-session-id'];
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
657
|
+
const user = this.getRequestUser(req);
|
|
658
|
+
let transport;
|
|
659
|
+
if (sessionId) {
|
|
660
|
+
transport = this.getSession(req, res, sessionId)?.transport;
|
|
661
|
+
if (!transport) {
|
|
614
662
|
return;
|
|
615
663
|
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
616
666
|
// No session yet: only an "initialize" request may create one.
|
|
617
667
|
if (!(0, types_js_1.isInitializeRequest)(req.body)) {
|
|
618
668
|
res.status(400).json({
|
|
@@ -627,8 +677,8 @@ class McpServer {
|
|
|
627
677
|
transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
628
678
|
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
|
|
629
679
|
onsessioninitialized: (sid) => {
|
|
630
|
-
this.sessions[sid] = { transport: transport, server, subscriptions };
|
|
631
|
-
this.adapter.log.debug(`MCP session initialized: ${sid}`);
|
|
680
|
+
this.sessions[sid] = { transport: transport, server, subscriptions, user };
|
|
681
|
+
this.adapter.log.debug(`MCP session initialized: ${sid} (user ${user})`);
|
|
632
682
|
},
|
|
633
683
|
});
|
|
634
684
|
transport.onclose = () => {
|
|
@@ -641,21 +691,20 @@ class McpServer {
|
|
|
641
691
|
};
|
|
642
692
|
await server.connect(transport);
|
|
643
693
|
}
|
|
644
|
-
|
|
694
|
+
const sessionTransport = transport;
|
|
695
|
+
await this.runAs(user, () => sessionTransport.handleRequest(req, res, req.body));
|
|
645
696
|
}
|
|
646
697
|
/** Handle server -> client SSE stream (GET) and session termination (DELETE). */
|
|
647
698
|
async handleMcpSessionRequest(req, res) {
|
|
648
699
|
const sessionId = req.headers['mcp-session-id'];
|
|
649
|
-
|
|
650
|
-
if (!transport) {
|
|
651
|
-
if (sessionId) {
|
|
652
|
-
this.sendSessionNotFound(res, sessionId);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
700
|
+
if (!sessionId) {
|
|
655
701
|
res.status(400).send('Invalid or missing session ID');
|
|
656
702
|
return;
|
|
657
703
|
}
|
|
658
|
-
|
|
704
|
+
const session = this.getSession(req, res, sessionId);
|
|
705
|
+
if (session) {
|
|
706
|
+
await this.runAs(session.user, () => session.transport.handleRequest(req, res));
|
|
707
|
+
}
|
|
659
708
|
}
|
|
660
709
|
/**
|
|
661
710
|
* Answer a request of an unknown session, e.g. after a restart of the adapter or when the session was closed.
|
|
@@ -1256,7 +1305,7 @@ class McpServer {
|
|
|
1256
1305
|
inputSchema: { id: zod_1.z.string().describe('Object ID, e.g. system.adapter.admin.0 or hm-rpc.0.ABC') },
|
|
1257
1306
|
}, async ({ id }) => {
|
|
1258
1307
|
try {
|
|
1259
|
-
const object = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
1308
|
+
const object = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
1260
1309
|
return ok({ ok: true, data: { object: object ?? null } });
|
|
1261
1310
|
}
|
|
1262
1311
|
catch (e) {
|
|
@@ -1300,7 +1349,7 @@ class McpServer {
|
|
|
1300
1349
|
}
|
|
1301
1350
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
1302
1351
|
const file = await this.adapter.readFileAsync(adapterName, fileName, {
|
|
1303
|
-
user: this.
|
|
1352
|
+
user: this.user,
|
|
1304
1353
|
});
|
|
1305
1354
|
const data = Buffer.isBuffer(file.file) ? file.file : Buffer.from(file.file || '');
|
|
1306
1355
|
const mimeType = file.mimeType || 'application/octet-stream';
|
|
@@ -1345,7 +1394,7 @@ class McpServer {
|
|
|
1345
1394
|
try {
|
|
1346
1395
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
1347
1396
|
const exists = await this.adapter.fileExistsAsync(adapterName, fileName, {
|
|
1348
|
-
user: this.
|
|
1397
|
+
user: this.user,
|
|
1349
1398
|
});
|
|
1350
1399
|
return ok({ ok: true, data: { path, exists: !!exists } });
|
|
1351
1400
|
}
|
|
@@ -1557,7 +1606,7 @@ class McpServer {
|
|
|
1557
1606
|
const upload = this.getUpload(upload_id);
|
|
1558
1607
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
1559
1608
|
await this.adapter.writeFileAsync(adapterName, fileName, node_fs_1.default.readFileSync(upload.file), {
|
|
1560
|
-
user: this.
|
|
1609
|
+
user: this.user,
|
|
1561
1610
|
});
|
|
1562
1611
|
this.removeTransfer(upload_id);
|
|
1563
1612
|
return ok({ ok: true, data: { path, size: upload.size } });
|
|
@@ -1582,7 +1631,7 @@ class McpServer {
|
|
|
1582
1631
|
}, async ({ path }) => {
|
|
1583
1632
|
try {
|
|
1584
1633
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
1585
|
-
await this.adapter.delFileAsync(adapterName, fileName, { user: this.
|
|
1634
|
+
await this.adapter.delFileAsync(adapterName, fileName, { user: this.user });
|
|
1586
1635
|
return ok({ ok: true, data: { path, deleted: true } });
|
|
1587
1636
|
}
|
|
1588
1637
|
catch (e) {
|
|
@@ -1628,7 +1677,7 @@ class McpServer {
|
|
|
1628
1677
|
if (!dirName) {
|
|
1629
1678
|
throw new Error('Path must contain a directory after the adapter, e.g. "0_userdata.0/my-folder"');
|
|
1630
1679
|
}
|
|
1631
|
-
await this.adapter.mkdirAsync(adapterName, dirName, { user: this.
|
|
1680
|
+
await this.adapter.mkdirAsync(adapterName, dirName, { user: this.user });
|
|
1632
1681
|
return ok({ ok: true, data: { path } });
|
|
1633
1682
|
}
|
|
1634
1683
|
catch (e) {
|
|
@@ -2203,7 +2252,7 @@ class McpServer {
|
|
|
2203
2252
|
mimeType: 'application/json',
|
|
2204
2253
|
}, async (uri, variables) => {
|
|
2205
2254
|
const id = decodeURIComponent(String(variables.id));
|
|
2206
|
-
const state = await this.adapter.getForeignStateAsync(id, { user: this.
|
|
2255
|
+
const state = await this.adapter.getForeignStateAsync(id, { user: this.user });
|
|
2207
2256
|
const body = state
|
|
2208
2257
|
? { id, val: state.val, ack: state.ack, ts: state.ts, lc: state.lc, q: state.q }
|
|
2209
2258
|
: { id, val: null };
|
|
@@ -2215,7 +2264,7 @@ class McpServer {
|
|
|
2215
2264
|
mimeType: 'application/json',
|
|
2216
2265
|
}, async (uri, variables) => {
|
|
2217
2266
|
const id = decodeURIComponent(String(variables.id));
|
|
2218
|
-
const obj = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
2267
|
+
const obj = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
2219
2268
|
return {
|
|
2220
2269
|
contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(obj ?? null) }],
|
|
2221
2270
|
};
|
|
@@ -2275,13 +2324,13 @@ class McpServer {
|
|
|
2275
2324
|
try {
|
|
2276
2325
|
if (id.includes('*')) {
|
|
2277
2326
|
// Wildcard pattern: expand to all matching states.
|
|
2278
|
-
const matches = await this.adapter.getForeignStatesAsync(id, { user: this.
|
|
2327
|
+
const matches = await this.adapter.getForeignStatesAsync(id, { user: this.user });
|
|
2279
2328
|
for (const matchId of Object.keys(matches || {})) {
|
|
2280
2329
|
states.push(McpServer.stateEntry(matchId, matches[matchId]));
|
|
2281
2330
|
}
|
|
2282
2331
|
}
|
|
2283
2332
|
else {
|
|
2284
|
-
const state = await this.adapter.getForeignStateAsync(id, { user: this.
|
|
2333
|
+
const state = await this.adapter.getForeignStateAsync(id, { user: this.user });
|
|
2285
2334
|
states.push(McpServer.stateEntry(id, state));
|
|
2286
2335
|
}
|
|
2287
2336
|
}
|
|
@@ -2299,7 +2348,7 @@ class McpServer {
|
|
|
2299
2348
|
}
|
|
2300
2349
|
/**
|
|
2301
2350
|
* Write multiple states as one operation: the write permission of every state is verified first, so a
|
|
2302
|
-
* batch containing a single state the
|
|
2351
|
+
* batch containing a single state the user may not write fails as a whole and nothing is
|
|
2303
2352
|
* written. Other failures of single states are reported per item and do not abort the rest.
|
|
2304
2353
|
*/
|
|
2305
2354
|
async setStates(items) {
|
|
@@ -2317,7 +2366,7 @@ class McpServer {
|
|
|
2317
2366
|
return results;
|
|
2318
2367
|
}
|
|
2319
2368
|
/**
|
|
2320
|
-
* Verify that the
|
|
2369
|
+
* Verify that the user of the request may write every given state, mirroring the checks js-controller runs in
|
|
2321
2370
|
* `setForeignState`: the user must be able to read the object (needed for the type coercion in
|
|
2322
2371
|
* {@link setState}) and the state ACL must allow writing — for an alias also on its write target.
|
|
2323
2372
|
*
|
|
@@ -2325,10 +2374,10 @@ class McpServer {
|
|
|
2325
2374
|
* @throws {Error} when at least one state may not be written; the message lists all of them
|
|
2326
2375
|
*/
|
|
2327
2376
|
async assertStatesWritable(ids) {
|
|
2328
|
-
if (this.
|
|
2377
|
+
if (this.user === 'system.user.admin') {
|
|
2329
2378
|
return;
|
|
2330
2379
|
}
|
|
2331
|
-
const permissions = await this.adapter.calculatePermissionsAsync(this.
|
|
2380
|
+
const permissions = await this.adapter.calculatePermissionsAsync(this.user, []);
|
|
2332
2381
|
if (permissions.groups?.includes('system.group.administrator')) {
|
|
2333
2382
|
return;
|
|
2334
2383
|
}
|
|
@@ -2336,7 +2385,7 @@ class McpServer {
|
|
|
2336
2385
|
for (const id of new Set(ids)) {
|
|
2337
2386
|
let obj;
|
|
2338
2387
|
try {
|
|
2339
|
-
obj = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
2388
|
+
obj = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
2340
2389
|
}
|
|
2341
2390
|
catch {
|
|
2342
2391
|
denied.push(id);
|
|
@@ -2355,14 +2404,14 @@ class McpServer {
|
|
|
2355
2404
|
}
|
|
2356
2405
|
}
|
|
2357
2406
|
if (denied.length) {
|
|
2358
|
-
throw new Error(`permissionError: user "${this.
|
|
2407
|
+
throw new Error(`permissionError: user "${this.user}" may not write ${denied.join(', ')} - nothing was written`);
|
|
2359
2408
|
}
|
|
2360
2409
|
}
|
|
2361
2410
|
/**
|
|
2362
2411
|
* The state ACL check of js-controller for `setState` on a single object.
|
|
2363
2412
|
*
|
|
2364
2413
|
* @param obj the state object (objects without ACL are not restricted by js-controller)
|
|
2365
|
-
* @param permissions the merged group permissions of the
|
|
2414
|
+
* @param permissions the merged group permissions of the user
|
|
2366
2415
|
*/
|
|
2367
2416
|
isStateWritable(obj, permissions) {
|
|
2368
2417
|
const acl = obj?.acl;
|
|
@@ -2374,7 +2423,7 @@ class McpServer {
|
|
|
2374
2423
|
return false;
|
|
2375
2424
|
}
|
|
2376
2425
|
let mask = 0x002; // everyone write
|
|
2377
|
-
if (acl.owner === this.
|
|
2426
|
+
if (acl.owner === this.user) {
|
|
2378
2427
|
mask = 0x200; // owner write
|
|
2379
2428
|
}
|
|
2380
2429
|
else if (acl.ownerGroup && permissions.groups?.includes(acl.ownerGroup)) {
|
|
@@ -2660,13 +2709,13 @@ class McpServer {
|
|
|
2660
2709
|
}
|
|
2661
2710
|
async getSystemInfo() {
|
|
2662
2711
|
const hostObj = await this.adapter.getForeignObjectAsync(`system.host.${this.adapter.host}`, {
|
|
2663
|
-
user: this.
|
|
2712
|
+
user: this.user,
|
|
2664
2713
|
});
|
|
2665
2714
|
const jsControllerVersion = hostObj?.common?.installedVersion || 'unknown';
|
|
2666
2715
|
const totalMem = Math.round(node_os_1.default.totalmem() / (1024 * 1024));
|
|
2667
2716
|
const freeMem = Math.round(node_os_1.default.freemem() / (1024 * 1024));
|
|
2668
2717
|
const instanceObjs = await this.adapter.getForeignObjectsAsync('system.adapter.*', 'instance', {
|
|
2669
|
-
user: this.
|
|
2718
|
+
user: this.user,
|
|
2670
2719
|
});
|
|
2671
2720
|
return {
|
|
2672
2721
|
js_controller: jsControllerVersion,
|
|
@@ -2684,7 +2733,7 @@ class McpServer {
|
|
|
2684
2733
|
const result = await this.adapter.getObjectListAsync({
|
|
2685
2734
|
startkey: '',
|
|
2686
2735
|
endkey: '\u9999',
|
|
2687
|
-
}, { sorted: true, user: this.
|
|
2736
|
+
}, { sorted: true, user: this.user });
|
|
2688
2737
|
const allObjects = result.rows;
|
|
2689
2738
|
const roomMembers = room ? await this.getEnumMembers('enum.rooms.', room) : null;
|
|
2690
2739
|
const needle = query.toLowerCase();
|
|
@@ -2720,7 +2769,7 @@ class McpServer {
|
|
|
2720
2769
|
}
|
|
2721
2770
|
async listDevices(params) {
|
|
2722
2771
|
const lang = params.language || this.language;
|
|
2723
|
-
const rooms = await (0, devices_1.getAiFriendlyStructure)(this.adapter, lang, { user: this.
|
|
2772
|
+
const rooms = await (0, devices_1.getAiFriendlyStructure)(this.adapter, lang, { user: this.user });
|
|
2724
2773
|
if (params.room) {
|
|
2725
2774
|
const needle = params.room.toLowerCase();
|
|
2726
2775
|
return rooms.filter(r => r.roomName.toLowerCase() === needle);
|
|
@@ -2750,20 +2799,20 @@ class McpServer {
|
|
|
2750
2799
|
options.step = step;
|
|
2751
2800
|
}
|
|
2752
2801
|
// GetHistoryOptions has no typed `user` field, but the controller honors it for ACL checks.
|
|
2753
|
-
options.user = this.
|
|
2802
|
+
options.user = this.user;
|
|
2754
2803
|
const res = await this.adapter.getHistoryAsync(params.id, options);
|
|
2755
2804
|
return { id: params.id, values: res?.result || [] };
|
|
2756
2805
|
}
|
|
2757
2806
|
async listInstances() {
|
|
2758
2807
|
const objs = await this.adapter.getForeignObjectsAsync('system.adapter.*', 'instance', {
|
|
2759
|
-
user: this.
|
|
2808
|
+
user: this.user,
|
|
2760
2809
|
});
|
|
2761
2810
|
const result = [];
|
|
2762
2811
|
for (const [fullId, obj] of Object.entries(objs || {})) {
|
|
2763
2812
|
const id = fullId.replace('system.adapter.', '');
|
|
2764
|
-
const aliveState = await this.adapter.getForeignStateAsync(`${fullId}.alive`, { user: this.
|
|
2813
|
+
const aliveState = await this.adapter.getForeignStateAsync(`${fullId}.alive`, { user: this.user });
|
|
2765
2814
|
const connState = await this.adapter.getForeignStateAsync(`${fullId}.connected`, {
|
|
2766
|
-
user: this.
|
|
2815
|
+
user: this.user,
|
|
2767
2816
|
});
|
|
2768
2817
|
result.push({
|
|
2769
2818
|
id,
|
|
@@ -2779,7 +2828,7 @@ class McpServer {
|
|
|
2779
2828
|
/** List installed adapters (system.adapter.<name> objects, no instances). */
|
|
2780
2829
|
async listAdapters(language) {
|
|
2781
2830
|
const lang = language || this.language;
|
|
2782
|
-
const view = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.香' }, { user: this.
|
|
2831
|
+
const view = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.香' }, { user: this.user });
|
|
2783
2832
|
return view.rows.map(row => {
|
|
2784
2833
|
const common = (row.value?.common || {});
|
|
2785
2834
|
return {
|
|
@@ -2806,7 +2855,7 @@ class McpServer {
|
|
|
2806
2855
|
return [];
|
|
2807
2856
|
}
|
|
2808
2857
|
// Determine which adapters are already installed.
|
|
2809
|
-
const installedView = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.香' }, { user: this.
|
|
2858
|
+
const installedView = await this.adapter.getObjectViewAsync('system', 'adapter', { startkey: 'system.adapter.', endkey: 'system.adapter.香' }, { user: this.user });
|
|
2810
2859
|
const installed = new Set(installedView.rows.map(row => row.value?.common?.name || row.id.replace('system.adapter.', '')));
|
|
2811
2860
|
const results = [];
|
|
2812
2861
|
for (const [name, entry] of Object.entries(json)) {
|
|
@@ -2844,10 +2893,10 @@ class McpServer {
|
|
|
2844
2893
|
return results;
|
|
2845
2894
|
}
|
|
2846
2895
|
async listHosts() {
|
|
2847
|
-
const objs = await this.adapter.getForeignObjectsAsync('system.host.*', 'host', { user: this.
|
|
2896
|
+
const objs = await this.adapter.getForeignObjectsAsync('system.host.*', 'host', { user: this.user });
|
|
2848
2897
|
const result = [];
|
|
2849
2898
|
for (const [fullId, obj] of Object.entries(objs || {})) {
|
|
2850
|
-
const aliveState = await this.adapter.getForeignStateAsync(`${fullId}.alive`, { user: this.
|
|
2899
|
+
const aliveState = await this.adapter.getForeignStateAsync(`${fullId}.alive`, { user: this.user });
|
|
2851
2900
|
result.push({
|
|
2852
2901
|
id: fullId.replace('system.host.', ''),
|
|
2853
2902
|
alive: !!aliveState?.val,
|
|
@@ -2862,7 +2911,7 @@ class McpServer {
|
|
|
2862
2911
|
* (ported from the ioBroker n8n node's `readIobEnums`).
|
|
2863
2912
|
*/
|
|
2864
2913
|
async readEnums(type, language, withIcons) {
|
|
2865
|
-
const enums = await this.adapter.getObjectViewAsync('system', 'enum', { startkey: `enum.${type}.`, endkey: `enum.${type}.香` }, { user: this.
|
|
2914
|
+
const enums = await this.adapter.getObjectViewAsync('system', 'enum', { startkey: `enum.${type}.`, endkey: `enum.${type}.香` }, { user: this.user });
|
|
2866
2915
|
const result = [];
|
|
2867
2916
|
// Cache member objects so a member shared by several enums is read only once.
|
|
2868
2917
|
const cache = {};
|
|
@@ -2882,7 +2931,7 @@ class McpServer {
|
|
|
2882
2931
|
if (obj === undefined) {
|
|
2883
2932
|
try {
|
|
2884
2933
|
cache[member] =
|
|
2885
|
-
(await this.adapter.getForeignObjectAsync(member, { user: this.
|
|
2934
|
+
(await this.adapter.getForeignObjectAsync(member, { user: this.user })) || false;
|
|
2886
2935
|
}
|
|
2887
2936
|
catch {
|
|
2888
2937
|
cache[member] = false;
|
|
@@ -2911,10 +2960,10 @@ class McpServer {
|
|
|
2911
2960
|
}
|
|
2912
2961
|
/** Write a state, coercing the value to the state's declared type (boolean/number/string). */
|
|
2913
2962
|
async setState(id, value, ack) {
|
|
2914
|
-
const obj = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
2963
|
+
const obj = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
2915
2964
|
const stateType = obj?.common?.type;
|
|
2916
2965
|
const coerced = McpServer.coerceValue(value, stateType);
|
|
2917
|
-
await this.adapter.setForeignStateAsync(id, coerced, ack, { user: this.
|
|
2966
|
+
await this.adapter.setForeignStateAsync(id, coerced, ack, { user: this.user });
|
|
2918
2967
|
return coerced;
|
|
2919
2968
|
}
|
|
2920
2969
|
/** Coerce an arbitrary value to the given ioBroker state type. */
|
|
@@ -2968,7 +3017,7 @@ class McpServer {
|
|
|
2968
3017
|
*/
|
|
2969
3018
|
async readFile(path, base64, offset = 0, length = DEFAULT_MAX_READ_BYTES) {
|
|
2970
3019
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
2971
|
-
const data = await this.adapter.readFileAsync(adapterName, fileName, { user: this.
|
|
3020
|
+
const data = await this.adapter.readFileAsync(adapterName, fileName, { user: this.user });
|
|
2972
3021
|
const file = data?.file;
|
|
2973
3022
|
const mimeType = data?.mimeType;
|
|
2974
3023
|
if (file === undefined || file === null) {
|
|
@@ -3014,12 +3063,12 @@ class McpServer {
|
|
|
3014
3063
|
async writeFile(path, content, base64) {
|
|
3015
3064
|
const { adapterName, fileName } = McpServer.parseFilePath(path);
|
|
3016
3065
|
const data = base64 ? Buffer.from(content, 'base64') : content;
|
|
3017
|
-
await this.adapter.writeFileAsync(adapterName, fileName, data, { user: this.
|
|
3066
|
+
await this.adapter.writeFileAsync(adapterName, fileName, data, { user: this.user });
|
|
3018
3067
|
}
|
|
3019
3068
|
/** List a directory in an adapter file storage. */
|
|
3020
3069
|
async listFiles(path) {
|
|
3021
3070
|
const { adapterName, dirName } = McpServer.parseDirPath(path);
|
|
3022
|
-
const entries = await this.adapter.readDirAsync(adapterName, dirName, { user: this.
|
|
3071
|
+
const entries = await this.adapter.readDirAsync(adapterName, dirName, { user: this.user });
|
|
3023
3072
|
return (entries || []).map(entry => ({
|
|
3024
3073
|
file: entry.file,
|
|
3025
3074
|
isDir: !!entry.isDir,
|
|
@@ -3034,11 +3083,11 @@ class McpServer {
|
|
|
3034
3083
|
if (from.adapterName !== to.adapterName) {
|
|
3035
3084
|
throw new Error('Renaming across adapter storages is not supported; both paths must share the adapter');
|
|
3036
3085
|
}
|
|
3037
|
-
await this.adapter.renameAsync(from.adapterName, from.fileName, to.fileName, { user: this.
|
|
3086
|
+
await this.adapter.renameAsync(from.adapterName, from.fileName, to.fileName, { user: this.user });
|
|
3038
3087
|
}
|
|
3039
3088
|
/** Create or update an object, merging common/native into an existing object (n8n `setIobObject`). */
|
|
3040
3089
|
async setObject(id, obj) {
|
|
3041
|
-
let existing = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
3090
|
+
let existing = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
3042
3091
|
if (existing) {
|
|
3043
3092
|
if (obj.common) {
|
|
3044
3093
|
existing.common = { ...existing.common, ...obj.common };
|
|
@@ -3050,7 +3099,7 @@ class McpServer {
|
|
|
3050
3099
|
else {
|
|
3051
3100
|
existing = obj;
|
|
3052
3101
|
}
|
|
3053
|
-
return this.adapter.setForeignObjectAsync(id, existing, { user: this.
|
|
3102
|
+
return this.adapter.setForeignObjectAsync(id, existing, { user: this.user });
|
|
3054
3103
|
}
|
|
3055
3104
|
/** Split a file path "<adapter>/<dir>/<file>" into adapter name and file name. */
|
|
3056
3105
|
static parseFilePath(path) {
|
|
@@ -3074,15 +3123,15 @@ class McpServer {
|
|
|
3074
3123
|
}
|
|
3075
3124
|
/** Delete an object, optionally with all its children. */
|
|
3076
3125
|
async deleteObject(id, recursive) {
|
|
3077
|
-
const existing = await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
3126
|
+
const existing = await this.adapter.getForeignObjectAsync(id, { user: this.user });
|
|
3078
3127
|
if (!existing) {
|
|
3079
3128
|
throw new Error(`Object "${id}" does not exist`);
|
|
3080
3129
|
}
|
|
3081
|
-
await this.adapter.delForeignObjectAsync(id, { user: this.
|
|
3130
|
+
await this.adapter.delForeignObjectAsync(id, { user: this.user, recursive: !!recursive });
|
|
3082
3131
|
}
|
|
3083
3132
|
/** Create a new state object; refuses to overwrite an existing object. */
|
|
3084
3133
|
async createState(params) {
|
|
3085
|
-
const existing = await this.adapter.getForeignObjectAsync(params.id, { user: this.
|
|
3134
|
+
const existing = await this.adapter.getForeignObjectAsync(params.id, { user: this.user });
|
|
3086
3135
|
if (existing) {
|
|
3087
3136
|
throw new Error(`Object "${params.id}" already exists - use set_object to modify it`);
|
|
3088
3137
|
}
|
|
@@ -3108,10 +3157,10 @@ class McpServer {
|
|
|
3108
3157
|
if (params.desc !== undefined) {
|
|
3109
3158
|
common.desc = params.desc;
|
|
3110
3159
|
}
|
|
3111
|
-
await this.adapter.setForeignObjectAsync(params.id, { type: 'state', common, native: {} }, { user: this.
|
|
3160
|
+
await this.adapter.setForeignObjectAsync(params.id, { type: 'state', common, native: {} }, { user: this.user });
|
|
3112
3161
|
if (params.def !== undefined) {
|
|
3113
3162
|
const coerced = McpServer.coerceValue(params.def, common.type);
|
|
3114
|
-
await this.adapter.setForeignStateAsync(params.id, coerced, true, { user: this.
|
|
3163
|
+
await this.adapter.setForeignStateAsync(params.id, coerced, true, { user: this.user });
|
|
3115
3164
|
}
|
|
3116
3165
|
return { id: params.id };
|
|
3117
3166
|
}
|
|
@@ -3119,7 +3168,7 @@ class McpServer {
|
|
|
3119
3168
|
async createScene(params) {
|
|
3120
3169
|
const instance = params.instance || 'scene.0';
|
|
3121
3170
|
const instanceObj = await this.adapter.getForeignObjectAsync(`system.adapter.${instance}`, {
|
|
3122
|
-
user: this.
|
|
3171
|
+
user: this.user,
|
|
3123
3172
|
});
|
|
3124
3173
|
if (!instanceObj) {
|
|
3125
3174
|
throw new Error(`Scene adapter instance "${instance}" is not installed. Install the ioBroker "scenes" adapter first.`);
|
|
@@ -3154,7 +3203,7 @@ class McpServer {
|
|
|
3154
3203
|
burstInterval: 0,
|
|
3155
3204
|
},
|
|
3156
3205
|
};
|
|
3157
|
-
await this.adapter.setForeignObjectAsync(id, sceneObj, { user: this.
|
|
3206
|
+
await this.adapter.setForeignObjectAsync(id, sceneObj, { user: this.user });
|
|
3158
3207
|
return { id, members: params.members.length };
|
|
3159
3208
|
}
|
|
3160
3209
|
/**
|
|
@@ -3192,7 +3241,7 @@ class McpServer {
|
|
|
3192
3241
|
let data;
|
|
3193
3242
|
if (params.file_path !== undefined) {
|
|
3194
3243
|
const { adapterName, fileName: storageName } = McpServer.parseFilePath(params.file_path);
|
|
3195
|
-
const result = await this.adapter.readFileAsync(adapterName, storageName, { user: this.
|
|
3244
|
+
const result = await this.adapter.readFileAsync(adapterName, storageName, { user: this.user });
|
|
3196
3245
|
data = Buffer.isBuffer(result.file) ? result.file : Buffer.from(result.file || '');
|
|
3197
3246
|
fileName = storageName.split('/').pop() || '';
|
|
3198
3247
|
}
|
|
@@ -3236,7 +3285,7 @@ class McpServer {
|
|
|
3236
3285
|
if (!name || /\s/.test(name)) {
|
|
3237
3286
|
throw new Error(`Invalid host "${name}"`);
|
|
3238
3287
|
}
|
|
3239
|
-
const hostObj = await this.adapter.getForeignObjectAsync(`system.host.${name}`, { user: this.
|
|
3288
|
+
const hostObj = await this.adapter.getForeignObjectAsync(`system.host.${name}`, { user: this.user });
|
|
3240
3289
|
if (hostObj?.type !== 'host') {
|
|
3241
3290
|
throw new Error(`Host "${name}" not found`);
|
|
3242
3291
|
}
|
|
@@ -3300,12 +3349,12 @@ class McpServer {
|
|
|
3300
3349
|
/** Like admin after an installation: touching the instance objects makes the controller restart them. */
|
|
3301
3350
|
async restartAdapterInstances(adapterName) {
|
|
3302
3351
|
const instances = await this.adapter.getForeignObjectsAsync(`system.adapter.${adapterName}.*`, 'instance', {
|
|
3303
|
-
user: this.
|
|
3352
|
+
user: this.user,
|
|
3304
3353
|
});
|
|
3305
3354
|
const restarted = [];
|
|
3306
3355
|
for (const [instanceId, instance] of Object.entries(instances || {})) {
|
|
3307
3356
|
if (instance?.common?.enabled) {
|
|
3308
|
-
await this.adapter.extendForeignObjectAsync(instanceId, {}, { user: this.
|
|
3357
|
+
await this.adapter.extendForeignObjectAsync(instanceId, {}, { user: this.user });
|
|
3309
3358
|
restarted.push(instanceId.replace('system.adapter.', ''));
|
|
3310
3359
|
}
|
|
3311
3360
|
}
|
|
@@ -3318,7 +3367,7 @@ class McpServer {
|
|
|
3318
3367
|
throw new Error(`Invalid instance "${id}", expected e.g. "hue.0"`);
|
|
3319
3368
|
}
|
|
3320
3369
|
const fullId = `system.adapter.${shortId}`;
|
|
3321
|
-
const obj = await this.adapter.getForeignObjectAsync(fullId, { user: this.
|
|
3370
|
+
const obj = await this.adapter.getForeignObjectAsync(fullId, { user: this.user });
|
|
3322
3371
|
if (obj?.type !== 'instance') {
|
|
3323
3372
|
throw new Error(`Instance "${shortId}" not found`);
|
|
3324
3373
|
}
|
|
@@ -3329,7 +3378,7 @@ class McpServer {
|
|
|
3329
3378
|
const instance = await this.getInstance(id);
|
|
3330
3379
|
const changed = !!instance.obj.common.enabled !== enabled;
|
|
3331
3380
|
if (changed) {
|
|
3332
|
-
await this.adapter.extendForeignObjectAsync(instance.fullId, { common: { enabled } }, { user: this.
|
|
3381
|
+
await this.adapter.extendForeignObjectAsync(instance.fullId, { common: { enabled } }, { user: this.user });
|
|
3333
3382
|
}
|
|
3334
3383
|
return { id: instance.id, enabled, changed };
|
|
3335
3384
|
}
|
|
@@ -3339,7 +3388,7 @@ class McpServer {
|
|
|
3339
3388
|
if (!instance.obj.common.enabled) {
|
|
3340
3389
|
throw new Error(`Instance "${instance.id}" is not enabled - start it instead`);
|
|
3341
3390
|
}
|
|
3342
|
-
await this.adapter.extendForeignObjectAsync(instance.fullId, {}, { user: this.
|
|
3391
|
+
await this.adapter.extendForeignObjectAsync(instance.fullId, {}, { user: this.user });
|
|
3343
3392
|
return { id: instance.id, restarted: true };
|
|
3344
3393
|
}
|
|
3345
3394
|
/**
|
|
@@ -3349,11 +3398,11 @@ class McpServer {
|
|
|
3349
3398
|
async setLogLevel(id, level, persistent) {
|
|
3350
3399
|
const instance = await this.getInstance(id);
|
|
3351
3400
|
if (persistent) {
|
|
3352
|
-
await this.adapter.extendForeignObjectAsync(instance.fullId, { common: { loglevel: level } }, { user: this.
|
|
3401
|
+
await this.adapter.extendForeignObjectAsync(instance.fullId, { common: { loglevel: level } }, { user: this.user });
|
|
3353
3402
|
}
|
|
3354
3403
|
else {
|
|
3355
3404
|
await this.adapter.setForeignStateAsync(`${instance.fullId}.logLevel`, level, false, {
|
|
3356
|
-
user: this.
|
|
3405
|
+
user: this.user,
|
|
3357
3406
|
});
|
|
3358
3407
|
}
|
|
3359
3408
|
return { id: instance.id, level, persistent: !!persistent };
|
|
@@ -3374,7 +3423,7 @@ class McpServer {
|
|
|
3374
3423
|
const adapterName = McpServer.checkAdapterName(params.adapter);
|
|
3375
3424
|
const host = await this.resolveHost(params.host);
|
|
3376
3425
|
if (params.instance !== undefined) {
|
|
3377
|
-
const existing = await this.adapter.getForeignObjectAsync(`system.adapter.${adapterName}.${params.instance}`, { user: this.
|
|
3426
|
+
const existing = await this.adapter.getForeignObjectAsync(`system.adapter.${adapterName}.${params.instance}`, { user: this.user });
|
|
3378
3427
|
if (existing) {
|
|
3379
3428
|
throw new Error(`Instance "${adapterName}.${params.instance}" already exists`);
|
|
3380
3429
|
}
|
|
@@ -3394,7 +3443,7 @@ class McpServer {
|
|
|
3394
3443
|
throw new Error(`Invalid version "${params.version}"`);
|
|
3395
3444
|
}
|
|
3396
3445
|
const adapterObj = await this.adapter.getForeignObjectAsync(`system.adapter.${adapterName}`, {
|
|
3397
|
-
user: this.
|
|
3446
|
+
user: this.user,
|
|
3398
3447
|
});
|
|
3399
3448
|
if (adapterObj?.type !== 'adapter') {
|
|
3400
3449
|
throw new Error(`Adapter "${adapterName}" is not installed - use add_instance to install it`);
|
|
@@ -3425,9 +3474,9 @@ class McpServer {
|
|
|
3425
3474
|
* matters when the user has custom repos active next to the default one.
|
|
3426
3475
|
*/
|
|
3427
3476
|
async getRepositoryJson() {
|
|
3428
|
-
const reposObj = await this.adapter.getForeignObjectAsync('system.repositories', { user: this.
|
|
3477
|
+
const reposObj = await this.adapter.getForeignObjectAsync('system.repositories', { user: this.user });
|
|
3429
3478
|
const repositories = (reposObj?.native?.repositories || {});
|
|
3430
|
-
const sysCfg = await this.adapter.getForeignObjectAsync('system.config', { user: this.
|
|
3479
|
+
const sysCfg = await this.adapter.getForeignObjectAsync('system.config', { user: this.user });
|
|
3431
3480
|
const active = sysCfg?.common?.activeRepo;
|
|
3432
3481
|
const activeNames = Array.isArray(active) ? active : active ? [active] : [];
|
|
3433
3482
|
const activeWithJson = activeNames.filter(name => repositories[name]?.json);
|
|
@@ -3520,7 +3569,7 @@ class McpServer {
|
|
|
3520
3569
|
/** Read a script object, failing if it is missing or not a script. */
|
|
3521
3570
|
async getScriptObject(id) {
|
|
3522
3571
|
const scriptId = McpServer.scriptId(id);
|
|
3523
|
-
const obj = await this.adapter.getForeignObjectAsync(scriptId, { user: this.
|
|
3572
|
+
const obj = await this.adapter.getForeignObjectAsync(scriptId, { user: this.user });
|
|
3524
3573
|
if (obj?.type !== 'script') {
|
|
3525
3574
|
throw new Error(`Script "${scriptId}" not found`);
|
|
3526
3575
|
}
|
|
@@ -3529,7 +3578,7 @@ class McpServer {
|
|
|
3529
3578
|
/** List the scripts of the javascript adapter (without their source). */
|
|
3530
3579
|
async listScripts(folder) {
|
|
3531
3580
|
const prefix = folder ? `script.js.${folder.replace(/^script\.js\./, '').replace(/\.$/, '')}.` : 'script.js.';
|
|
3532
|
-
const objs = await this.adapter.getForeignObjectsAsync(`${prefix}*`, 'script', { user: this.
|
|
3581
|
+
const objs = await this.adapter.getForeignObjectsAsync(`${prefix}*`, 'script', { user: this.user });
|
|
3533
3582
|
return Object.entries(objs || {}).map(([id, obj]) => this.scriptInfo(id, obj));
|
|
3534
3583
|
}
|
|
3535
3584
|
/**
|
|
@@ -3538,7 +3587,7 @@ class McpServer {
|
|
|
3538
3587
|
*/
|
|
3539
3588
|
async setScript(params) {
|
|
3540
3589
|
const scriptId = McpServer.scriptId(params.id);
|
|
3541
|
-
const existing = await this.adapter.getForeignObjectAsync(scriptId, { user: this.
|
|
3590
|
+
const existing = await this.adapter.getForeignObjectAsync(scriptId, { user: this.user });
|
|
3542
3591
|
if (existing && existing.type !== 'script') {
|
|
3543
3592
|
throw new Error(`"${scriptId}" exists and is not a script`);
|
|
3544
3593
|
}
|
|
@@ -3554,7 +3603,7 @@ class McpServer {
|
|
|
3554
3603
|
if (params.enabled !== undefined) {
|
|
3555
3604
|
common.enabled = params.enabled;
|
|
3556
3605
|
}
|
|
3557
|
-
await this.adapter.extendForeignObjectAsync(scriptId, { common }, { user: this.
|
|
3606
|
+
await this.adapter.extendForeignObjectAsync(scriptId, { common }, { user: this.user });
|
|
3558
3607
|
return { id: scriptId, created: false };
|
|
3559
3608
|
}
|
|
3560
3609
|
const engine = (await this.getInstance(params.instance || 'javascript.0')).id;
|
|
@@ -3562,8 +3611,8 @@ class McpServer {
|
|
|
3562
3611
|
const segments = scriptId.split('.');
|
|
3563
3612
|
for (let i = 3; i < segments.length; i++) {
|
|
3564
3613
|
const folderId = segments.slice(0, i).join('.');
|
|
3565
|
-
if (!(await this.adapter.getForeignObjectAsync(folderId, { user: this.
|
|
3566
|
-
await this.adapter.setForeignObjectAsync(folderId, { type: 'channel', common: { name: segments[i - 1] }, native: {} }, { user: this.
|
|
3614
|
+
if (!(await this.adapter.getForeignObjectAsync(folderId, { user: this.user }))) {
|
|
3615
|
+
await this.adapter.setForeignObjectAsync(folderId, { type: 'channel', common: { name: segments[i - 1] }, native: {} }, { user: this.user });
|
|
3567
3616
|
}
|
|
3568
3617
|
}
|
|
3569
3618
|
await this.adapter.setForeignObjectAsync(scriptId, {
|
|
@@ -3579,19 +3628,19 @@ class McpServer {
|
|
|
3579
3628
|
verbose: false,
|
|
3580
3629
|
},
|
|
3581
3630
|
native: {},
|
|
3582
|
-
}, { user: this.
|
|
3631
|
+
}, { user: this.user });
|
|
3583
3632
|
return { id: scriptId, created: true };
|
|
3584
3633
|
}
|
|
3585
3634
|
/** Enable (start) or disable (stop) a script. */
|
|
3586
3635
|
async enableScript(id, enabled) {
|
|
3587
3636
|
const script = await this.getScriptObject(id);
|
|
3588
|
-
await this.adapter.extendForeignObjectAsync(script.id, { common: { enabled } }, { user: this.
|
|
3637
|
+
await this.adapter.extendForeignObjectAsync(script.id, { common: { enabled } }, { user: this.user });
|
|
3589
3638
|
return { id: script.id, enabled };
|
|
3590
3639
|
}
|
|
3591
3640
|
/** Delete a script. */
|
|
3592
3641
|
async deleteScript(id) {
|
|
3593
3642
|
const script = await this.getScriptObject(id);
|
|
3594
|
-
await this.adapter.delForeignObjectAsync(script.id, { user: this.
|
|
3643
|
+
await this.adapter.delForeignObjectAsync(script.id, { user: this.user });
|
|
3595
3644
|
return { id: script.id, deleted: true };
|
|
3596
3645
|
}
|
|
3597
3646
|
/**
|
|
@@ -3601,7 +3650,7 @@ class McpServer {
|
|
|
3601
3650
|
async runScript(params) {
|
|
3602
3651
|
const instance = await this.getInstance(params.instance || 'javascript.0');
|
|
3603
3652
|
const timeout = params.timeout ?? 5000;
|
|
3604
|
-
const result = await this.adapter.sendToAsync(instance.id, 'execute', { source: params.source, engineType: params.engineType, timeout, verbose: false, logLevel: 'info' }, { timeout: timeout + 15_000, user: this.
|
|
3653
|
+
const result = await this.adapter.sendToAsync(instance.id, 'execute', { source: params.source, engineType: params.engineType, timeout, verbose: false, logLevel: 'info' }, { timeout: timeout + 15_000, user: this.user });
|
|
3605
3654
|
if (!result || typeof result !== 'object') {
|
|
3606
3655
|
throw new Error(`"${instance.id}" did not answer - javascript adapter 10.1.0 or newer is required`);
|
|
3607
3656
|
}
|
|
@@ -3621,7 +3670,7 @@ class McpServer {
|
|
|
3621
3670
|
if (!this.isSendToAllowed(instance.id)) {
|
|
3622
3671
|
throw new Error(`"${instance.id}" is not in the allowlist of send_to`);
|
|
3623
3672
|
}
|
|
3624
|
-
const response = await this.adapter.sendToAsync(instance.id, params.command, (params.message ?? {}), { timeout: params.timeout ?? 10_000, user: this.
|
|
3673
|
+
const response = await this.adapter.sendToAsync(instance.id, params.command, (params.message ?? {}), { timeout: params.timeout ?? 10_000, user: this.user });
|
|
3625
3674
|
return { instance: instance.id, command: params.command, response: response ?? null };
|
|
3626
3675
|
}
|
|
3627
3676
|
/**
|
|
@@ -3633,16 +3682,16 @@ class McpServer {
|
|
|
3633
3682
|
if (!/^alias\.\d+\.[^*\s]+$/.test(id)) {
|
|
3634
3683
|
throw new Error(`Invalid alias id "${params.id}"`);
|
|
3635
3684
|
}
|
|
3636
|
-
if (await this.adapter.getForeignObjectAsync(id, { user: this.
|
|
3685
|
+
if (await this.adapter.getForeignObjectAsync(id, { user: this.user })) {
|
|
3637
3686
|
throw new Error(`Object "${id}" already exists`);
|
|
3638
3687
|
}
|
|
3639
|
-
const target = await this.adapter.getForeignObjectAsync(params.target, { user: this.
|
|
3688
|
+
const target = await this.adapter.getForeignObjectAsync(params.target, { user: this.user });
|
|
3640
3689
|
if (target?.type !== 'state') {
|
|
3641
3690
|
throw new Error(`Target "${params.target}" is not a state`);
|
|
3642
3691
|
}
|
|
3643
3692
|
if (params.write_target) {
|
|
3644
3693
|
const writeTarget = await this.adapter.getForeignObjectAsync(params.write_target, {
|
|
3645
|
-
user: this.
|
|
3694
|
+
user: this.user,
|
|
3646
3695
|
});
|
|
3647
3696
|
if (writeTarget?.type !== 'state') {
|
|
3648
3697
|
throw new Error(`Write target "${params.write_target}" is not a state`);
|
|
@@ -3667,7 +3716,7 @@ class McpServer {
|
|
|
3667
3716
|
if (params.unit !== undefined || source.unit !== undefined) {
|
|
3668
3717
|
common.unit = params.unit ?? source.unit;
|
|
3669
3718
|
}
|
|
3670
|
-
await this.adapter.setForeignObjectAsync(id, { type: 'state', common, native: {} }, { user: this.
|
|
3719
|
+
await this.adapter.setForeignObjectAsync(id, { type: 'state', common, native: {} }, { user: this.user });
|
|
3671
3720
|
return { id, target: params.target, ...(params.write_target ? { write_target: params.write_target } : {}) };
|
|
3672
3721
|
}
|
|
3673
3722
|
/**
|
|
@@ -3684,7 +3733,8 @@ class McpServer {
|
|
|
3684
3733
|
const token = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
3685
3734
|
// One folder per link, so the file keeps its name (`npm` needs the .tgz extension)
|
|
3686
3735
|
const folder = node_path_1.default.join(this.transferDir, token);
|
|
3687
|
-
|
|
3736
|
+
// recursive: another MCP server may have removed our folder as stale (see removeStaleTransferDirs)
|
|
3737
|
+
node_fs_1.default.mkdirSync(folder, { recursive: true });
|
|
3688
3738
|
const file = node_path_1.default.join(folder, name.replace(/[^a-zA-Z0-9._-]/g, '_') || 'file');
|
|
3689
3739
|
if (data !== undefined) {
|
|
3690
3740
|
node_fs_1.default.writeFileSync(file, data);
|
|
@@ -3699,6 +3749,35 @@ class McpServer {
|
|
|
3699
3749
|
});
|
|
3700
3750
|
return token;
|
|
3701
3751
|
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Remove the transfer folders that a process left behind when it was killed. Other MCP servers on this
|
|
3754
|
+
* host (web extension, standalone, admin) use the same temp folder, so only folders unchanged for longer
|
|
3755
|
+
* than a link lives are removed: every new link creates a subfolder and so touches its folder.
|
|
3756
|
+
*/
|
|
3757
|
+
removeStaleTransferDirs() {
|
|
3758
|
+
const tmp = node_os_1.default.tmpdir();
|
|
3759
|
+
try {
|
|
3760
|
+
for (const name of node_fs_1.default.readdirSync(tmp)) {
|
|
3761
|
+
if (!name.startsWith('iobroker-mcp-')) {
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
const dir = node_path_1.default.join(tmp, name);
|
|
3765
|
+
try {
|
|
3766
|
+
const stat = node_fs_1.default.statSync(dir);
|
|
3767
|
+
if (stat.isDirectory() && Date.now() - stat.mtimeMs > TRANSFER_TTL) {
|
|
3768
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
3769
|
+
this.adapter.log.debug(`Removed stale transfer folder "${dir}"`);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
catch (e) {
|
|
3773
|
+
this.adapter.log.debug(`Cannot remove stale transfer folder "${dir}": ${e}`);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
catch (e) {
|
|
3778
|
+
this.adapter.log.debug(`Cannot read "${tmp}": ${e}`);
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3702
3781
|
/** The public URL of a download or upload link. */
|
|
3703
3782
|
transferUrl(baseUrl, token) {
|
|
3704
3783
|
return `${baseUrl}${this.mcpPath}/transfer/${token}`;
|
|
@@ -3805,7 +3884,7 @@ class McpServer {
|
|
|
3805
3884
|
// --- helpers ---
|
|
3806
3885
|
/** Resolve the member ids of a room/function enum matched by id or (localized) name. */
|
|
3807
3886
|
async getEnumMembers(prefix, nameOrId) {
|
|
3808
|
-
const enums = await this.adapter.getForeignObjectsAsync(`${prefix}*`, 'enum', { user: this.
|
|
3887
|
+
const enums = await this.adapter.getForeignObjectsAsync(`${prefix}*`, 'enum', { user: this.user });
|
|
3809
3888
|
const needle = nameOrId.toLowerCase();
|
|
3810
3889
|
for (const [id, obj] of Object.entries(enums || {})) {
|
|
3811
3890
|
if (id.toLowerCase() === needle || this.getName(obj?.common?.name).toLowerCase() === needle) {
|