@iobroker/mcp-server 1.7.1 → 1.8.1

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 CHANGED
@@ -157,6 +157,27 @@ All options that change the system are **off** by default; turn on only what the
157
157
  `.tgz` on a test system. An installed package runs with full rights on the host, so never enable it on a
158
158
  productive system or for clients you do not fully trust.
159
159
 
160
+ **Rights of the ioBroker user**
161
+ The options above decide which tools exist; what a call may do is decided by the rights of the ioBroker user the
162
+ MCP session runs as (the logged-in user, or the Default User without authentication), like in ioBroker.admin:
163
+ - the ACLs of objects, states and files for reading and writing them,
164
+ - the "other" rights of the user's groups: **execute** for commands on a host (`install_package`, `add_instance`,
165
+ `delete_instance`, `upgrade_adapter`) and **sendto** for messages to adapters (`send_to`, `run_script`).
166
+ Without them the call is refused before anything runs. Administrators have all rights.
167
+
168
+ A session is not even offered the tools that need a right the groups of its user lack: without the write right for
169
+ states no `set_state`/`set_states`, without the write right for objects no `set_object`, `create_state`,
170
+ `start_instance`, ..., without the write right for files no `write_file`, without **execute** no `install_package`,
171
+ without **sendto** no `send_to`. The rights are read when the session starts; every call is checked anyway.
172
+
173
+ `check_permissions` shows these rights and the ACLs of given objects.
174
+
175
+ **Log**
176
+ Every call of a changing tool is logged at info level with the user, the tool and its arguments
177
+ (`MCP system.user.admin: set_state(id="hue.0.light.state", value=true)`), and every failed call as a warning with
178
+ the same details. Long values are shortened; file contents, script code and `send_to` payloads are replaced by
179
+ their size.
180
+
160
181
  ### SSL/TLS Configuration
161
182
  - **Enable HTTPS**: Enable HTTPS/SSL for secure connections
162
183
  - **Public Certificate**: Path to the public certificate file
@@ -178,6 +199,8 @@ Point your MCP client at:
178
199
  | Tool | Description |
179
200
  |-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
180
201
  | `get_states` | Retrieve the current value of one or multiple states; IDs may contain wildcards (e.g. `hue.0.*.brightness`); an ID without a state is returned with `exists: false` or a hint |
202
+ | `wait_for_state` | Wait until a state has a `value` (or, without `value`, until it is updated), optionally only with `ack`; gives up after `timeout` seconds (default 30, at most 50) with `reached: false` |
203
+ | `find_orphan_states` | Find states without object (e.g. `system.adapter.vis.0.alive` of a deleted instance) by an ID `pattern`, except the upload progress `system.adapter.<adapter>.upload`, which has no object on purpose; delete them on the host with `iobroker state del <id>`|
181
204
  | `get_object` | Read a single object by its ID |
182
205
  | `check_permissions` | Show the user and groups of the MCP session, the rights of its groups and, for given object IDs, whether it may read/write/delete the object and read/write the state |
183
206
  | `search_objects` | Search objects/states by keyword (matched against ID and name); optional filters for object `type`, `role`, `room` and source `adapter` instance |
@@ -192,7 +215,7 @@ Point your MCP client at:
192
215
  | `read_file` | Read a file from an adapter file storage (optional base64); large files are read in chunks via `offset`/`length` (default 512 KiB per call) — the result carries `size`, `truncated` and `nextOffset`; `download: true` returns a download link instead |
193
216
  | `list_files` | List a directory in an adapter file storage |
194
217
  | `file_exists` | Check whether a file exists in an adapter file storage |
195
- | `get_logs` | Retrieve recent ioBroker log lines; optional filters by `level` (error/warn/info/debug), source `adapter` and start time (`from_ts`); `logFile` tells the time of the newest line and the file size |
218
+ | `get_logs` | Retrieve recent ioBroker log lines; optional filters by `level` (error/warn/info/debug), source `adapter`, start time (`from_ts`) and `search` in the message (text, or `/regex/flags`); `logFile` tells the time of the newest line and the file size |
196
219
  | `write_log` | Write a message to the ioBroker log |
197
220
  | `system_info` | Get system and js-controller information |
198
221
  | `list_adapter_updates` | List installed adapters that have a newer version in the active repository |
@@ -200,7 +223,7 @@ Point your MCP client at:
200
223
  | `list_scripts` | List the scripts of the javascript adapter (without source) |
201
224
  | `get_script` | Read a script including its source |
202
225
  | `ping_host` | Diagnose connectivity to a network device: ICMP ping to `host` plus an optional TCP connect to `port` — useful to investigate adapter `ETIMEDOUT`/connection errors |
203
- | `set_state` | Set the value of a state (value coerced to the state type) — requires *Allow setting states* |
226
+ | `set_state` | Set the value of a state (value coerced to the state type); with `wait_ack` (seconds) it waits until the device confirms the command (`ack: true`) and returns the confirmed state — requires *Allow setting states* |
204
227
  | `set_states` | Set multiple states in one call (for scenes/group actions like "all lights off") — requires *Allow setting states* |
205
228
  | `clear_notifications` | Acknowledge system notifications, optionally of one scope/category/instance — requires *Allow setting states* |
206
229
  | `set_object` | Create/update an object (merges common/native) — requires *Allow object/file changes* |
@@ -227,8 +250,9 @@ Point your MCP client at:
227
250
  | `run_script` | Run code once in the javascript adapter and return its log output (javascript ≥ 10.1.0) — requires *Allow changing scripts* |
228
251
  | `send_to` | Send a message to an adapter instance (`sendTo`) and return the answer — only instances in *Allow messages to adapters* |
229
252
 
230
- All object/state access runs with the permissions of the configured **Default User**. The write tools are only
231
- registered when their respective permission option is enabled.
253
+ All object/state access runs with the permissions of the user of the session (the logged-in user or the
254
+ configured **Default User**). The write tools are only registered when their respective permission option is
255
+ enabled, and only offered to users whose groups have the rights they need.
232
256
 
233
257
  Every tool declares a JSON input schema and all four MCP behaviour hints (`readOnlyHint`, `destructiveHint`,
234
258
  `idempotentHint`, `openWorldHint`), so a host can tell what a tool does before calling it and can ask the user
@@ -311,6 +335,19 @@ client calls it and its result is verified:
311
335
  Placeholder for the next version (at the beginning of the line):
312
336
  ### **WORK IN PROGRESS**
313
337
  -->
338
+ ### 1.8.1 (2026-10-26)
339
+ * (@GermanBluefox) `find_orphan_states` no longer reports the upload progress of the adapters (`system.adapter.<adapter>.upload`), which js-controller writes without object on purpose
340
+
341
+ ### 1.8.0 (2026-10-26)
342
+ * (@GermanBluefox) Security: commands on a host (`install_package`, `add_instance`, `delete_instance`, `upgrade_adapter`) require the "execute" right of the user, messages to adapters (`send_to`, `run_script`) the "sendto" right, like in admin. Before, they ran with the rights of the adapter
343
+ * (@GermanBluefox) `install_package` reports a done installation even if the instances could not be restarted afterwards (`restartError`), instead of failing
344
+ * (@GermanBluefox) Every call of a changing tool is logged with the user, the tool and its arguments; a failed call names the tool and its arguments
345
+ * (@GermanBluefox) `get_logs` searches the messages for a text or a regular expression (`search`)
346
+ * (@GermanBluefox) A session is only offered the tools its user has the rights for, e.g. no `install_package` without "execute"
347
+ * (@GermanBluefox) `set_state` waits for the confirmation of the device with `wait_ack` and returns the confirmed state
348
+ * (@GermanBluefox) Added the `wait_for_state` tool: waits until a state has a value or is updated
349
+ * (@GermanBluefox) Added the `find_orphan_states` tool: finds states without object, e.g. of deleted instances
350
+
314
351
  ### 1.7.1 (2026-10-26)
315
352
  * (@GermanBluefox) Fixed: `get_logs` always returned `size: null` for the log file, because the host sends the size as a string
316
353
 
@@ -79,7 +79,7 @@ export interface InProcessMcpOptions {
79
79
  }
80
80
  /** A ready-to-use in-process MCP connection: list tools, call them, and close everything. */
81
81
  export interface InProcessMcp {
82
- /** List the available tools (already filtered by the configured permission toggles). */
82
+ /** List the available tools (already filtered by the configured permission toggles and the user rights). */
83
83
  listTools(): Promise<InProcessToolInfo[]>;
84
84
  /** Call a tool by name with the given arguments; returns its text result. */
85
85
  callTool(name: string, args?: Record<string, unknown>): Promise<InProcessToolResult>;
@@ -26,6 +26,7 @@ async function createInProcessMcp(options) {
26
26
  const { clientName, clientVersion, ...embedOptions } = options;
27
27
  const mcp = index_1.default.createEmbedded(embedOptions);
28
28
  const server = mcp.createInProcessServer();
29
+ await mcp.hideToolsWithoutRights(server);
29
30
  const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
30
31
  await server.connect(serverTransport);
31
32
  const client = new index_js_1.Client({
@@ -1 +1 @@
1
- {"version":3,"file":"inProcessClient.js","sourceRoot":"","sources":["../src/inProcessClient.ts"],"names":[],"mappings":";;;;;AAsHA,gDAmDC;AAzKD;;;;;;;;GAQG;AACH,wEAAmE;AACnE,uEAA0E;AAC1E,oDAAgC;AAqGhC;;;;;GAKG;AACI,KAAK,UAAU,kBAAkB,CAAC,OAA4B;IACjE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC;IAC/D,MAAM,GAAG,GAAG,eAAS,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;IAC3C,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,GAAG,+BAAiB,CAAC,gBAAgB,EAAE,CAAC;IAChF,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,IAAI,iBAAM,CACrB;QACI,IAAI,EAAE,UAAU,IAAI,2BAA2B;QAC/C,OAAO,EAAE,aAAa,IAAI,OAAO;KACpC,EACD,EAAE,YAAY,EAAE,EAAE,EAAE,CACvB,CAAC;IACF,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEtC,OAAO;QACH,KAAK,CAAC,SAAS;YACX,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAClC,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,WAAW,EAAG,IAAI,CAAC,WAAuC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;gBAChG,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,WAAW,EAAE,IAAI,CAAC,WAAW;aAChC,CAAC,CAAC,CAAC;QACR,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,IAA8B;YACvD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;YACnE,MAAM,MAAM,GAAI,GAAG,CAAC,OAA0C,IAAI,EAAE,CAAC;YACrE,MAAM,IAAI,GAAG,MAAM;iBACd,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;iBACxE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAc,CAAC;iBAClC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAC5C,CAAC;QACD,KAAK,CAAC,KAAK;YACP,IAAI,CAAC;gBACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YACD,IAAI,CAAC;gBACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YACD,GAAG,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;KACJ,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"inProcessClient.js","sourceRoot":"","sources":["../src/inProcessClient.ts"],"names":[],"mappings":";;;;;AAsHA,gDAoDC;AA1KD;;;;;;;;GAQG;AACH,wEAAmE;AACnE,uEAA0E;AAC1E,oDAAgC;AAqGhC;;;;;GAKG;AACI,KAAK,UAAU,kBAAkB,CAAC,OAA4B;IACjE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC;IAC/D,MAAM,GAAG,GAAG,eAAS,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;IAC3C,MAAM,GAAG,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,GAAG,+BAAiB,CAAC,gBAAgB,EAAE,CAAC;IAChF,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,IAAI,iBAAM,CACrB;QACI,IAAI,EAAE,UAAU,IAAI,2BAA2B;QAC/C,OAAO,EAAE,aAAa,IAAI,OAAO;KACpC,EACD,EAAE,YAAY,EAAE,EAAE,EAAE,CACvB,CAAC;IACF,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAEtC,OAAO;QACH,KAAK,CAAC,SAAS;YACX,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAClC,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,WAAW,EAAG,IAAI,CAAC,WAAuC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE;gBAChG,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,WAAW,EAAE,IAAI,CAAC,WAAW;aAChC,CAAC,CAAC,CAAC;QACR,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,IAAY,EAAE,IAA8B;YACvD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;YACnE,MAAM,MAAM,GAAI,GAAG,CAAC,OAA0C,IAAI,EAAE,CAAC;YACrE,MAAM,IAAI,GAAG,MAAM;iBACd,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;iBACxE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAc,CAAC;iBAClC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAC5C,CAAC;QACD,KAAK,CAAC,KAAK;YACP,IAAI,CAAC;gBACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YACD,IAAI,CAAC;gBACD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACL,SAAS;YACb,CAAC;YACD,GAAG,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;KACJ,CAAC;AACN,CAAC"}
package/build/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export default class McpServer {
14
14
  private readonly subscriptionCounts;
15
15
  /** Recent log lines kept for `ioblog://` resource reads (filled while there are log subscribers). */
16
16
  private readonly logBuffer;
17
+ /** The tools of every server created by {@link createServer}, for {@link hideToolsWithoutRights}. */
18
+ private readonly registeredTools;
17
19
  private config;
18
20
  private readonly extension;
19
21
  private readonly routerPrefix;
@@ -68,9 +70,21 @@ export default class McpServer {
68
70
  * Build a fresh MCP SDK server with all ioBroker tools registered, ready to be connected to an
69
71
  * arbitrary transport. Used for in-process embedding (e.g. by ioBroker.admin) over an
70
72
  * InMemoryTransport, where there is no HTTP server/session layer. The caller owns the lifecycle
71
- * of the returned server (connect it to a transport, and `close()` it when done).
73
+ * of the returned server (connect it to a transport, and `close()` it when done). Call
74
+ * {@link hideToolsWithoutRights} before connecting it to offer only the tools the user may use.
72
75
  */
73
76
  createInProcessServer(): McpSdkServer;
77
+ /**
78
+ * Do not offer the tools the user cannot use, because its groups lack a general right the tool needs (e.g.
79
+ * `install_package` without "execute"), so the client does not try them in vain. Administrators get all
80
+ * tools. The rights are read once, before the server is connected: changed rights apply to new sessions,
81
+ * and every call is checked anyway.
82
+ *
83
+ * @param server a server returned by {@link createInProcessServer}
84
+ * @param user the ioBroker user of the session (default: the configured default user)
85
+ * @returns the names of the hidden tools
86
+ */
87
+ hideToolsWithoutRights(server: McpSdkServer, user?: `system.user.${string}`): Promise<string[]>;
74
88
  /**
75
89
  * Convenience factory for embedding the MCP server inside another adapter's process (no HTTP).
76
90
  * The returned instance is not wired to any Express app; obtain a tool server via
@@ -212,6 +226,22 @@ export default class McpServer {
212
226
  private refUnsubscribe;
213
227
  /** Release all subscriptions held by a session (on close). */
214
228
  private cleanupSession;
229
+ /**
230
+ * Remember every tool registered on `server` afterwards (for {@link hideToolsWithoutRights}) and log its
231
+ * calls: a failed call as a warning with the tool and its arguments, a successful call of a writing tool
232
+ * (not `readOnlyHint`) at info level, so the log shows who changed what.
233
+ *
234
+ * @param server the server whose `registerTool` is wrapped
235
+ */
236
+ private trackTools;
237
+ /**
238
+ * The arguments of a tool call for the log: long values are shortened, and contents that may hold secrets
239
+ * or large data (file contents, script code, message payloads) are replaced by their size.
240
+ *
241
+ * @param tool name of the tool
242
+ * @param args arguments of the call
243
+ */
244
+ private static describeArgs;
215
245
  /**
216
246
  * Create a new MCP SDK server with all ioBroker tools registered.
217
247
  *
@@ -230,6 +260,16 @@ export default class McpServer {
230
260
  * written. Other failures of single states are reported per item and do not abort the rest.
231
261
  */
232
262
  private setStates;
263
+ /**
264
+ * Verify a right of the "other" category of the user, like admin does for `cmdExec` (`execute`) and
265
+ * `sendTo` (`sendto`). js-controller does not check them for messages, so without this an MCP user
266
+ * could run what the ioBroker user may not.
267
+ *
268
+ * @param operation `execute` for CLI commands on a host, `sendto` for messages to adapters
269
+ * @param action what was denied, for the error message (e.g. `run "iobroker url ..." on host "x"`)
270
+ * @throws {Error} a permissionError before anything was done
271
+ */
272
+ private assertOtherPermission;
233
273
  /**
234
274
  * Verify that the user of the request may write every given state, mirroring the checks js-controller runs in
235
275
  * `setForeignState`: the user must be able to read the object (needed for the type coercion in
@@ -327,6 +367,31 @@ export default class McpServer {
327
367
  private readEnums;
328
368
  /** Write a state, coercing the value to the state's declared type (boolean/number/string). */
329
369
  private setState;
370
+ /**
371
+ * Read a state until `reached` accepts it, the time is up or the client cancels the call.
372
+ *
373
+ * @param id state ID
374
+ * @param reached the condition
375
+ * @param timeoutSeconds how long to wait at most
376
+ * @param signal aborted when the client cancels the call
377
+ * @returns whether the condition was met, the last read state and how long it took (ms)
378
+ */
379
+ private waitForState;
380
+ /**
381
+ * Whether a state value is the expected one, also if one of them is stored as a string ("21.5" and 21.5,
382
+ * '{"a":1}' and {a: 1}).
383
+ */
384
+ private static sameValue;
385
+ /**
386
+ * The states without object. For a pattern, js-controller returns only the states of existing state objects,
387
+ * except in its maintenance mode (only for the admin user), which reads the keys of the states database. The
388
+ * objects are read with the rights of the adapter, as an object the user may not see is still an object. A
389
+ * state without object has no ACL, so js-controller lets every user read it anyway.
390
+ *
391
+ * @param pattern IDs of the states to check, wildcards allowed
392
+ * @returns how many states were checked, and the states without object
393
+ */
394
+ private findOrphanStates;
330
395
  /** Coerce an arbitrary value to the given ioBroker state type. */
331
396
  private static coerceValue;
332
397
  /**
@@ -384,7 +449,12 @@ export default class McpServer {
384
449
  private runHostCommand;
385
450
  /** Request something from a host (`getNotifications`, ...) and wait for the answer. */
386
451
  private hostRequest;
387
- /** Like admin after an installation: touching the instance objects makes the controller restart them. */
452
+ /**
453
+ * Like admin after an installation: touching the instance objects makes the controller restart them.
454
+ *
455
+ * @param adapterName adapter whose enabled instances are restarted
456
+ * @param restarted receives the ids of the restarted instances, also those before an error
457
+ */
388
458
  private restartAdapterInstances;
389
459
  /** Read an instance object by its short id (`hue.0`) or full id (`system.adapter.hue.0`). */
390
460
  private getInstance;