suprema-biostar-mcp 1.0.1__py3-none-any.whl

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.
Files changed (61) hide show
  1. biostar_x_mcp_server/__init__.py +25 -0
  2. biostar_x_mcp_server/__main__.py +15 -0
  3. biostar_x_mcp_server/config.py +87 -0
  4. biostar_x_mcp_server/handlers/__init__.py +35 -0
  5. biostar_x_mcp_server/handlers/access_handler.py +2162 -0
  6. biostar_x_mcp_server/handlers/audit_handler.py +489 -0
  7. biostar_x_mcp_server/handlers/auth_handler.py +216 -0
  8. biostar_x_mcp_server/handlers/base_handler.py +228 -0
  9. biostar_x_mcp_server/handlers/card_handler.py +746 -0
  10. biostar_x_mcp_server/handlers/device_handler.py +4344 -0
  11. biostar_x_mcp_server/handlers/door_handler.py +3969 -0
  12. biostar_x_mcp_server/handlers/event_handler.py +1331 -0
  13. biostar_x_mcp_server/handlers/file_handler.py +212 -0
  14. biostar_x_mcp_server/handlers/help_web_handler.py +379 -0
  15. biostar_x_mcp_server/handlers/log_handler.py +1051 -0
  16. biostar_x_mcp_server/handlers/navigation_handler.py +109 -0
  17. biostar_x_mcp_server/handlers/occupancy_handler.py +541 -0
  18. biostar_x_mcp_server/handlers/user_handler.py +3568 -0
  19. biostar_x_mcp_server/schemas/__init__.py +21 -0
  20. biostar_x_mcp_server/schemas/access.py +158 -0
  21. biostar_x_mcp_server/schemas/audit.py +73 -0
  22. biostar_x_mcp_server/schemas/auth.py +24 -0
  23. biostar_x_mcp_server/schemas/cards.py +128 -0
  24. biostar_x_mcp_server/schemas/devices.py +496 -0
  25. biostar_x_mcp_server/schemas/doors.py +306 -0
  26. biostar_x_mcp_server/schemas/events.py +104 -0
  27. biostar_x_mcp_server/schemas/files.py +7 -0
  28. biostar_x_mcp_server/schemas/help.py +29 -0
  29. biostar_x_mcp_server/schemas/logs.py +33 -0
  30. biostar_x_mcp_server/schemas/occupancy.py +19 -0
  31. biostar_x_mcp_server/schemas/tool_response.py +29 -0
  32. biostar_x_mcp_server/schemas/users.py +166 -0
  33. biostar_x_mcp_server/server.py +335 -0
  34. biostar_x_mcp_server/session.py +221 -0
  35. biostar_x_mcp_server/tool_manager.py +172 -0
  36. biostar_x_mcp_server/tools/__init__.py +45 -0
  37. biostar_x_mcp_server/tools/access.py +510 -0
  38. biostar_x_mcp_server/tools/audit.py +227 -0
  39. biostar_x_mcp_server/tools/auth.py +59 -0
  40. biostar_x_mcp_server/tools/cards.py +269 -0
  41. biostar_x_mcp_server/tools/categories.py +197 -0
  42. biostar_x_mcp_server/tools/devices.py +1552 -0
  43. biostar_x_mcp_server/tools/doors.py +865 -0
  44. biostar_x_mcp_server/tools/events.py +305 -0
  45. biostar_x_mcp_server/tools/files.py +28 -0
  46. biostar_x_mcp_server/tools/help.py +80 -0
  47. biostar_x_mcp_server/tools/logs.py +123 -0
  48. biostar_x_mcp_server/tools/navigation.py +89 -0
  49. biostar_x_mcp_server/tools/occupancy.py +91 -0
  50. biostar_x_mcp_server/tools/users.py +1113 -0
  51. biostar_x_mcp_server/utils/__init__.py +31 -0
  52. biostar_x_mcp_server/utils/category_mapper.py +206 -0
  53. biostar_x_mcp_server/utils/decorators.py +101 -0
  54. biostar_x_mcp_server/utils/language_detector.py +51 -0
  55. biostar_x_mcp_server/utils/search.py +42 -0
  56. biostar_x_mcp_server/utils/timezone.py +122 -0
  57. suprema_biostar_mcp-1.0.1.dist-info/METADATA +163 -0
  58. suprema_biostar_mcp-1.0.1.dist-info/RECORD +61 -0
  59. suprema_biostar_mcp-1.0.1.dist-info/WHEEL +4 -0
  60. suprema_biostar_mcp-1.0.1.dist-info/entry_points.txt +2 -0
  61. suprema_biostar_mcp-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1552 @@
1
+ from mcp.types import Tool
2
+
3
+ # Device management tools
4
+ LIST_DEVICES_TOOL = Tool(
5
+ name="list-devices",
6
+ description="Retrieve a list of all devices from the BioStar 2 system",
7
+ inputSchema={
8
+ "type": "object",
9
+ "properties": {
10
+ "group_id": {
11
+ "type": "integer",
12
+ "description": "Filter devices by device group ID"
13
+ },
14
+ "device_type": {
15
+ "type": "string",
16
+ "description": "Filter by device type (e.g., 'BioStation', 'BioEntry', 'FaceStation')"
17
+ }
18
+ },
19
+ "required": []
20
+ }
21
+ )
22
+
23
+ GET_DEVICE_TOOL = Tool(
24
+ name="get-device",
25
+ description="Get detailed information about a specific device",
26
+ inputSchema={
27
+ "type": "object",
28
+ "properties": {
29
+ "device_id": {
30
+ "type": "integer",
31
+ "description": "ID of the device to retrieve"
32
+ }
33
+ },
34
+ "required": ["device_id"]
35
+ }
36
+ )
37
+
38
+ ADD_DEVICE_TOOL = Tool(
39
+ name="add-device",
40
+ description="Add a new device to the BioStar 2 system. It's recommended to run 'udp-search' first to discover devices on the network",
41
+ inputSchema={
42
+ "type": "object",
43
+ "properties": {
44
+ "device_id": {
45
+ "type": "integer",
46
+ "description": "Unique device ID"
47
+ },
48
+ "name": {
49
+ "type": "string",
50
+ "description": "Name of the device"
51
+ },
52
+ "device_type_id": {
53
+ "type": "integer",
54
+ "description": "Device type ID (e.g., 8 for BioStation 2, 9 for BioStation A2, 10 for FaceStation 2, 35 for BioStation 3)"
55
+ },
56
+ "device_type_name": {
57
+ "type": "string",
58
+ "description": "Device type name (e.g., 'BioStation 2', 'BioStation A2', 'FaceStation 2')"
59
+ },
60
+ "ip_address": {
61
+ "type": "string",
62
+ "description": "IP address of the device"
63
+ },
64
+ "port": {
65
+ "type": "integer",
66
+ "description": "Port number (default: 51211)",
67
+ "default": 51211
68
+ },
69
+ "use_ssl": {
70
+ "type": "boolean",
71
+ "description": "Whether to use SSL connection",
72
+ "default": False
73
+ },
74
+ "device_group_id": {
75
+ "type": "integer",
76
+ "description": "ID of the device group to add this device to"
77
+ }
78
+ },
79
+ "required": ["device_id", "name", "ip_address", "device_group_id"]
80
+ }
81
+ )
82
+
83
+ UPDATE_DEVICE_TOOL = Tool(
84
+ name="update-device",
85
+ description="Update device configuration and settings",
86
+ inputSchema={
87
+ "type": "object",
88
+ "properties": {
89
+ "device_id": {
90
+ "type": "integer",
91
+ "description": "ID of the device to update"
92
+ },
93
+ "name": {
94
+ "type": "string",
95
+ "description": "New name for the device"
96
+ },
97
+ "description": {
98
+ "type": "string",
99
+ "description": "Device description"
100
+ },
101
+ "time_zone": {
102
+ "type": "string",
103
+ "description": "Time zone for the device (e.g., 'America/New_York')"
104
+ },
105
+ "volume": {
106
+ "type": "integer",
107
+ "description": "Device volume level (0-100)"
108
+ }
109
+ },
110
+ "required": ["device_id"]
111
+ }
112
+ )
113
+
114
+ REBOOT_DEVICE_TOOL = Tool(
115
+ name="reboot-device",
116
+ description=(
117
+ "Restart one or more devices via POST /api/devices/reset.\n"
118
+ "- Body: {\"DeviceCollection\":{\"rows\":[{\"id\":<num>}, ...], \"total\":<n>}}\n"
119
+ "- Success when HTTP 200 AND (Response.code == \"0\" if present) AND each row code == \"0\".\n"
120
+ "- STRICT NAME MODE: If 'device_names' is used, names must match EXACTLY (case-sensitive).\n"
121
+ " 0 match -> no action; return full list and needs_selection.\n"
122
+ " >1 match (duplicate names) -> no action; return candidates and needs_selection.\n"
123
+ "- If 'device_search_text' is provided (and no exact selection), returns candidates ONLY (no action).\n"
124
+ "- If both 'device_ids' and 'device_names' are provided, they must resolve to the SAME set.\n"
125
+ "- If more than one target, 'confirm_multi' must be true."
126
+ ),
127
+ inputSchema={
128
+ "type": "object",
129
+ "properties": {
130
+ "device_ids": {
131
+ "type": "array",
132
+ "items": {"oneOf": [{"type": "integer"}, {"type": "string"}]},
133
+ "description": "One or more device IDs to restart (existence validated)."
134
+ },
135
+ "device_id": {
136
+ "type": "integer",
137
+ "description": "Legacy single device id input."
138
+ },
139
+ "device_names": {
140
+ "type": "array",
141
+ "items": {"type": "string"},
142
+ "description": "Device names to restart (EXACT, case-sensitive; no fuzzy)."
143
+ },
144
+ "device_search_text": {
145
+ "type": "string",
146
+ "description": "Browse candidates by substring (case-insensitive). Returns candidates only; no action."
147
+ },
148
+ "confirm_multi": {
149
+ "type": "boolean",
150
+ "default": False,
151
+ "description": "Must be true to act on more than one device."
152
+ }
153
+ },
154
+ "required": []
155
+ }
156
+ )
157
+
158
+ GET_DEVICE_STATUS_TOOL = Tool(
159
+ name="get-device-status",
160
+ description="Get the current status and health of a device",
161
+ inputSchema={
162
+ "type": "object",
163
+ "properties": {
164
+ "device_id": {
165
+ "type": "integer",
166
+ "description": "ID of the device to check status"
167
+ }
168
+ },
169
+ "required": ["device_id"]
170
+ }
171
+ )
172
+
173
+ SCAN_DEVICES_TOOL = Tool(
174
+ name="scan-devices",
175
+ description="Scan network for BioStar 2 compatible devices",
176
+ inputSchema={
177
+ "type": "object",
178
+ "properties": {
179
+ "network_range": {
180
+ "type": "string",
181
+ "description": "IP range to scan (e.g., '192.168.1.0/24')"
182
+ },
183
+ "port": {
184
+ "type": "integer",
185
+ "description": "Port to scan (default: 51211)",
186
+ "default": 51211
187
+ }
188
+ },
189
+ "required": []
190
+ }
191
+ )
192
+
193
+ SYNC_DEVICE_TIME_TOOL = Tool(
194
+ name="sync-device-time",
195
+ description="Synchronize device time with server time",
196
+ inputSchema={
197
+ "type": "object",
198
+ "properties": {
199
+ "device_id": {
200
+ "type": "integer",
201
+ "description": "ID of the device to sync time"
202
+ }
203
+ },
204
+ "required": ["device_id"]
205
+ }
206
+ )
207
+
208
+ CLEAR_DEVICE_LOG_TOOL = Tool(
209
+ name="clear-device-log",
210
+ description=" CRITICAL: Clear logs from a specific device. THIS IS PERMANENT and CANNOT BE UNDONE! Logs are critical for audit and security. ALWAYS confirm with user before executing. For demo data, REQUIRE explicit 'delete confirm' or '삭제 확인' from user.",
211
+ inputSchema={
212
+ "type": "object",
213
+ "properties": {
214
+ "device_id": {
215
+ "type": "integer",
216
+ "description": "ID of the device"
217
+ },
218
+ "log_type": {
219
+ "type": "string",
220
+ "description": "Type of logs to clear",
221
+ "enum": ["all", "event", "image"],
222
+ "default": "all"
223
+ }
224
+ },
225
+ "required": ["device_id"]
226
+ }
227
+ )
228
+
229
+ LOCK_DEVICE_TOOL = Tool(
230
+ name="lock-device",
231
+ description="Lock a device to prevent access",
232
+ inputSchema={
233
+ "type": "object",
234
+ "properties": {
235
+ "device_id": {
236
+ "type": "integer",
237
+ "description": "ID of the device to lock"
238
+ }
239
+ },
240
+ "required": ["device_id"]
241
+ }
242
+ )
243
+
244
+ UNLOCK_DEVICE_TOOL = Tool(
245
+ name="unlock-device",
246
+ description="Unlock a device to allow normal operation",
247
+ inputSchema={
248
+ "type": "object",
249
+ "properties": {
250
+ "device_id": {
251
+ "type": "integer",
252
+ "description": "ID of the device to unlock"
253
+ }
254
+ },
255
+ "required": ["device_id"]
256
+ }
257
+ )
258
+
259
+ TCP_SEARCH_TOOL = Tool(
260
+ name="tcp-search",
261
+ description="Search for a specific BioStar device using TCP connection. This is the PREFERRED method for adding devices when you know the IP address. Use this BEFORE udp-search.",
262
+ inputSchema={
263
+ "type": "object",
264
+ "properties": {
265
+ "ip_address": {
266
+ "type": "string",
267
+ "description": "IP address of the device to connect to"
268
+ },
269
+ "port": {
270
+ "type": "integer",
271
+ "description": "Port number (default: 51211)",
272
+ "default": 51211
273
+ },
274
+ "timeout": {
275
+ "type": "integer",
276
+ "description": "Connection timeout in seconds (default: 10)",
277
+ "default": 10
278
+ },
279
+ "retry_count": {
280
+ "type": "integer",
281
+ "description": "Number of retry attempts (default: 2)",
282
+ "default": 2
283
+ }
284
+ },
285
+ "required": ["ip_address"]
286
+ }
287
+ )
288
+
289
+ UDP_SEARCH_TOOL = Tool(
290
+ name="udp-search",
291
+ description="Search for BioStar devices on the network using UDP broadcast. Returns only disconnected devices (server_connected.status = 'disconnected') that can be connected to the system. Use this ONLY when tcp-search fails.",
292
+ inputSchema={
293
+ "type": "object",
294
+ "properties": {
295
+ "timeout": {
296
+ "type": "integer",
297
+ "description": "Search timeout in seconds (default: 5)",
298
+ "default": 5
299
+ }
300
+ },
301
+ "required": []
302
+ }
303
+ )
304
+
305
+ DISCONNECT_DEVICE_TOOL = Tool(
306
+ name="disconnect-device",
307
+ description="Disconnect a device from the BioStar 2 server",
308
+ inputSchema={
309
+ "type": "object",
310
+ "properties": {
311
+ "device_id": {
312
+ "type": "integer",
313
+ "description": "ID of the device to disconnect"
314
+ }
315
+ },
316
+ "required": ["device_id"]
317
+ }
318
+ )
319
+
320
+ REMOVE_DEVICE_TOOL = Tool(
321
+ name="remove-device",
322
+ description=" CRITICAL: Remove a device from the BioStar 2 system permanently. THIS IS PERMANENT and CANNOT BE UNDONE! ALWAYS confirm with user before executing. For demo data, REQUIRE explicit 'delete confirm' or '삭제 확인' from user.",
323
+ inputSchema={
324
+ "type": "object",
325
+ "properties": {
326
+ "device_id": {
327
+ "type": "integer",
328
+ "description": "ID of the device to remove"
329
+ }
330
+ },
331
+ "required": ["device_id"]
332
+ }
333
+ )
334
+
335
+ # Device Group management tools
336
+ GET_DEVICE_GROUPS_TOOL = Tool(
337
+ name="get-device-groups",
338
+ description=(
339
+ "List all device groups using POST /api/v2/device_groups/search with payload "
340
+ '{"order_by":"depth:false"}. Returns id, name, depth, parent_id, is_root, '
341
+ "and is_top_level. Optionally filter by name substring."
342
+ ),
343
+ inputSchema={
344
+ "type": "object",
345
+ "properties": {
346
+ "group_search_text": {
347
+ "type": "string",
348
+ "description": "Case-insensitive substring filter on group name"
349
+ }
350
+ },
351
+ "required": []
352
+ }
353
+ )
354
+
355
+
356
+ GET_DEVICE_GROUP_TOOL = Tool(
357
+ name="get-device-group",
358
+ description=(
359
+ "Get a device group by STRICT selection and (if uniquely resolved) return full details via GET /api/device_groups/{id}.\n"
360
+ "- Inputs:\n"
361
+ " - group_id (preferred), OR\n"
362
+ " - group_name (EXACT, case-sensitive), OR\n"
363
+ " - group_search_text (substring browse; returns candidates ONLY)\n"
364
+ "- Branch behavior:\n"
365
+ " - 0 match -> return all available groups (compact) and needs_selection=true.\n"
366
+ " - 1 match -> fetch detail (devices, child groups, parent, is_top_level).\n"
367
+ " - >1 match -> return candidates (id/name/depth/parent) and needs_selection=true."
368
+ ),
369
+ inputSchema={
370
+ "type": "object",
371
+ "properties": {
372
+ "group_id": {
373
+ "type": "integer",
374
+ "description": "Exact device group ID to fetch."
375
+ },
376
+ "group_name": {
377
+ "type": "string",
378
+ "description": "EXACT (case-sensitive) device group name."
379
+ },
380
+ "group_search_text": {
381
+ "type": "string",
382
+ "description": "Substring browse for group names. Returns candidates only."
383
+ }
384
+ },
385
+ "required": []
386
+ }
387
+ )
388
+
389
+ CREATE_DEVICE_GROUP_TOOL = Tool(
390
+ name="create-device-group",
391
+ description="Create a new device group",
392
+ inputSchema={
393
+ "type": "object",
394
+ "properties": {
395
+ "name": {
396
+ "type": "string",
397
+ "description": "Name of the device group"
398
+ },
399
+ "description": {
400
+ "type": "string",
401
+ "description": "Description of the device group"
402
+ },
403
+ "parent_id": {
404
+ "type": "integer",
405
+ "description": "Parent device group ID (default: 1 for root group)",
406
+ "default": 1
407
+ },
408
+ "device_ids": {
409
+ "type": "array",
410
+ "items": {"type": "integer"},
411
+ "description": "List of device IDs to include in the group"
412
+ }
413
+ },
414
+ "required": ["name"]
415
+ }
416
+ )
417
+
418
+ UPDATE_DEVICE_GROUP_TOOL = Tool(
419
+ name="update-device-group",
420
+ description="Update an existing device group",
421
+ inputSchema={
422
+ "type": "object",
423
+ "properties": {
424
+ "group_id": {
425
+ "type": "integer",
426
+ "description": "ID of the device group to update"
427
+ },
428
+ "name": {
429
+ "type": "string",
430
+ "description": "New name for the device group"
431
+ },
432
+ "description": {
433
+ "type": "string",
434
+ "description": "New description for the device group"
435
+ },
436
+ "device_ids": {
437
+ "type": "array",
438
+ "items": {"type": "integer"},
439
+ "description": "New list of device IDs for the group"
440
+ }
441
+ },
442
+ "required": ["group_id"]
443
+ }
444
+ )
445
+
446
+ DELETE_DEVICE_GROUP_TOOL = Tool(
447
+ name="delete-device-group",
448
+ description="Delete a device group",
449
+ inputSchema={
450
+ "type": "object",
451
+ "properties": {
452
+ "group_id": {
453
+ "type": "integer",
454
+ "description": "ID of the device group to delete"
455
+ }
456
+ },
457
+ "required": ["group_id"]
458
+ }
459
+ )
460
+
461
+ MOVE_DEVICE_TO_GROUP_TOOL = Tool(
462
+ name="move-device-to-group",
463
+ description="Move a device into a specific device group. Resolves device/group by id or name substring.",
464
+ inputSchema={
465
+ "type": "object",
466
+ "properties": {
467
+ "device_id": {"type": "integer", "description": "Device ID (preferred if known)"},
468
+ "device_name": {"type": "string", "description": "Device name (substring match)"},
469
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
470
+ "target_group_id": {"type": "integer", "description": "Target device group ID"},
471
+ "target_group_name": {"type": "string", "description": "Target device group name (substring match)"},
472
+ "group_search_text": {"type": "string", "description": "Alternate group query (substring)"}
473
+ },
474
+ "anyOf": [
475
+ {"required": ["device_id", "target_group_id"]},
476
+ {"required": ["device_id", "target_group_name"]},
477
+ {"required": ["device_name", "target_group_id"]},
478
+ {"required": ["device_name", "target_group_name"]},
479
+ {"required": ["device_search_text", "target_group_name"]},
480
+ {"required": ["device_search_text", "target_group_id"]}
481
+ ]
482
+ }
483
+ )
484
+
485
+ REMOVE_DEVICE_FROM_GROUP_TOOL = Tool(
486
+ name="remove-device-from-group",
487
+ description="Move a device to the root device group (id=1). Resolves device by id or name substring.",
488
+ inputSchema={
489
+ "type": "object",
490
+ "properties": {
491
+ "device_id": {"type": "integer", "description": "Device ID (preferred if known)"},
492
+ "device_name": {"type": "string", "description": "Device name (substring match)"},
493
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"}
494
+ },
495
+ "anyOf": [
496
+ {"required": ["device_id"]},
497
+ {"required": ["device_name"]},
498
+ {"required": ["device_search_text"]}
499
+ ]
500
+ }
501
+ )
502
+
503
+ UPDATE_DEVICE_AUTH_MODE_TOOL = Tool(
504
+ name="update-device-auth-mode",
505
+ description=(
506
+ "Set/append/remove authentication operation modes on a device. "
507
+ "Resolve target by device_id/device_name/door_id/door_name. "
508
+ "When both mode_codes and auth_specs are given, mode_codes take precedence."
509
+ ),
510
+ inputSchema={
511
+ "type": "object",
512
+ "properties": {
513
+ "device_id": {"type": "integer", "description": "Target device id to update"},
514
+ "device_name": {"type": "string", "description": "Target device name"},
515
+ "door_id": {"type": "integer", "description": "Door id; entry device will be used"},
516
+ "door_name": {"type": "string", "description": "Door name; entry device will be used"},
517
+ "mode_codes": {
518
+ "type": "array",
519
+ "items": {"type": "integer"},
520
+ "description": "Extended auth mode codes (11..49). Takes precedence over 'auth_specs'."
521
+ },
522
+ "auth_specs": {
523
+ "type": "array",
524
+ "items": {"type": "string"},
525
+ "description": "Specs like 'card+face+fingerprint' or 'face+pin' (ASCII-only)."
526
+ },
527
+ "action": {
528
+ "type": "string",
529
+ "enum": ["set", "add", "remove"],
530
+ "default": "set",
531
+ "description": "How to apply target modes."
532
+ },
533
+ "conflict_policy": {
534
+ "type": "string",
535
+ "enum": ["overwrite", "abort", "merge"],
536
+ "default": "overwrite",
537
+ "description": "Policy when 'add' conflicts with business rules."
538
+ },
539
+ "schedule_id": {
540
+ "type": "integer",
541
+ "default": 1,
542
+ "description": "Schedule id for each operation mode row."
543
+ }
544
+ },
545
+ "anyOf": [
546
+ {"required": ["device_id"]},
547
+ {"required": ["device_name"]},
548
+ {"required": ["door_id"]},
549
+ {"required": ["door_name"]}
550
+ ],
551
+ "oneOf": [
552
+ {"required": ["mode_codes"]},
553
+ {"required": ["auth_specs"]}
554
+ ]
555
+ }
556
+ )
557
+
558
+ UPDATE_DEVICE_FACE_DISTANCE_TOOL = Tool(
559
+ name="update-device-face-distance",
560
+ description=(
561
+ "Update face detection distance only (min fixed to 30cm). "
562
+ "Max snaps to steps 30..130, or 255 when 'over_130' is requested. "
563
+ "Resolve target by device_id/device_name/door_id/door_name."
564
+ ),
565
+ inputSchema={
566
+ "type": "object",
567
+ "properties": {
568
+ "device_id": {"type": "integer", "description": "Target device id to update"},
569
+ "device_name": {"type": "string", "description": "Target device name"},
570
+ "door_id": {"type": "integer", "description": "Door id; entry device will be used"},
571
+ "door_name": {"type": "string", "description": "Door name; entry device will be used"},
572
+ "max_cm": {
573
+ "oneOf": [
574
+ {"type": "integer", "enum": [30,40,50,60,70,80,90,100,110,120,130]},
575
+ {"type": "string", "enum": ["over_130"]}
576
+ ],
577
+ "description": "One of 30,40,...,130 or 'over_130' (->255)."
578
+ }
579
+ },
580
+ "anyOf": [
581
+ {"required": ["device_id"]},
582
+ {"required": ["device_name"]},
583
+ {"required": ["door_id"]},
584
+ {"required": ["door_name"]}
585
+ ],
586
+ "required": ["max_cm"]
587
+ }
588
+ )
589
+
590
+ UPDATE_DEVICE_DATE_TYPE_TOOL = Tool(
591
+ name="update-device-date-type",
592
+ description=(
593
+ "Update only the device's display date type via PUT /api/devices/{id}.\n"
594
+ "- Accepts either 'date_type' (0|1|2) or 'date_format' "
595
+ "(\"YYYY/MM/DD\" | \"MM/DD/YYYY\" | \"DD/MM/YYYY\").\n"
596
+ "- Resolution: device_id OR device_name OR door_id OR door_name "
597
+ "(also supports device_search_text via the handler's resolver).\n"
598
+ "- Sends a minimal payload: {\"Device\": {\"display\": {\"date_type\": \"<code>\"}}}.\n"
599
+ "- Does NOT modify any other fields."
600
+ ),
601
+ inputSchema={
602
+ "type": "object",
603
+ "properties": {
604
+ # ---- target resolution (same policy as other device update tools) ----
605
+ "device_id": {"type": "integer", "description": "Target device id"},
606
+ "device_name": {"type": "string", "description": "Target device name (exact or substring depending on resolver)"},
607
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
608
+ "door_id": {"type": "integer", "description": "Door id; entry device will be used unless 'edge' says otherwise"},
609
+ "door_name": {"type": "string", "description": "Door name (substring) -> entry/exit device"},
610
+ "edge": {
611
+ "type": "string",
612
+ "enum": ["entry", "exit"],
613
+ "description": "When resolving by door, choose entry or exit device (default: entry)"
614
+ },
615
+
616
+ # ---- date type selection ----
617
+ "date_type": {
618
+ "oneOf": [
619
+ {"type": "integer", "enum": [0, 1, 2]},
620
+ {"type": "string", "enum": ["0", "1", "2"]}
621
+ ],
622
+ "description": "Date type code: 0=YYYY/MM/DD, 1=MM/DD/YYYY, 2=DD/MM/YYYY"
623
+ },
624
+ "date_format": {
625
+ "type": "string",
626
+ "enum": ["YYYY/MM/DD", "MM/DD/YYYY", "DD/MM/YYYY"],
627
+ "description": "Alternative to 'date_type'. Will be mapped to 0|1|2."
628
+ }
629
+ },
630
+ "anyOf": [
631
+ {"required": ["device_id"]},
632
+ {"required": ["device_name"]},
633
+ {"required": ["door_id"]},
634
+ {"required": ["door_name"]},
635
+ {"required": ["device_search_text"]}
636
+ ],
637
+ "oneOf": [
638
+ {"required": ["date_type"]},
639
+ {"required": ["date_format"]}
640
+ ]
641
+ }
642
+ )
643
+
644
+
645
+ UPDATE_DEVICE_TIMEZONE_TOOL = Tool(
646
+ name="update-device-timezone",
647
+ description=(
648
+ "Update only Device.system.timezone with a fixed UTC offset (seconds). "
649
+ "Accepts one of: offset_seconds (int), utc_offset string like '+09:00'/'-08:30', "
650
+ "or tz_label (Korean dropdown label, city/abbr like '서울','테헤란','하와이','PST'). "
651
+ "This tool fetches the current device, merges the timezone value, and PUTs back so all other settings remain intact."
652
+ ),
653
+ inputSchema={
654
+ "type": "object",
655
+ "properties": {
656
+ # --- Target resolution (strict) ---
657
+ "device_id": {"type": "integer", "description": "Target device ID (preferred)"},
658
+ "device_name": {"type": "string", "description": "Exact device name (strict match)"},
659
+ "door_id": {"type": "integer", "description": "Resolve via door's entry device"},
660
+ "door_name": {"type": "string", "description": "Resolve via door's entry device (exact)"},
661
+
662
+ # --- Timezone spec (one-of) ---
663
+ "offset_seconds": {
664
+ "type": "integer",
665
+ "description": "Final UTC offset in seconds (e.g., 32400 for UTC+9, -28800 for UTC-8)."
666
+ },
667
+ "utc_offset": {
668
+ "type": "string",
669
+ "description": "UTC offset string like '+09:00', '-08:00', '+03:30', etc."
670
+ },
671
+ "tz_label": {
672
+ "type": "string",
673
+ "description": "Dropdown label or alias (Korean), or short city/abbr like '서울','하와이','테헤란','PST'."
674
+ },
675
+
676
+ # --- Optional dry-run ---
677
+ "dry_run": {
678
+ "type": "boolean",
679
+ "default": False,
680
+ "description": "If true, do not PUT; only resolve and return the computed offset."
681
+ }
682
+ },
683
+ "anyOf": [
684
+ {"required": ["device_id"]},
685
+ {"required": ["device_name"]},
686
+ {"required": ["door_id"]},
687
+ {"required": ["door_name"]}
688
+ ],
689
+ "oneOf": [
690
+ {"required": ["offset_seconds"]},
691
+ {"required": ["utc_offset"]},
692
+ {"required": ["tz_label"]}
693
+ ]
694
+ }
695
+ )
696
+
697
+ FACTORY_RESET_DEVICE_TOOL = Tool(
698
+ name="factory-reset-device",
699
+ description=(
700
+ " EXTREMELY DANGEROUS Factory-reset a device via POST /api/devices/factory_reset.\n"
701
+ " THIS ERASES ALL DATA ON THE DEVICE INCLUDING USERS, LOGS, AND SETTINGS!\n"
702
+ " THIS IS PERMANENT and CANNOT BE UNDONE!\n"
703
+ " ALWAYS ASK USER TWICE FOR CONFIRMATION BEFORE EXECUTING!\n"
704
+ " For demo data, REQUIRE explicit 'factory reset confirm' or '초기화 확인' from user.\n\n"
705
+ "- Resolves target by device_id OR device_name OR door_id OR door_name "
706
+ "(optionally edge=entry|exit for door resolution).\n"
707
+ "- Preflight checks if the device is linked to any door (entry/exit). "
708
+ "If linked, this tool returns a failure without calling the reset API.\n"
709
+ "- Sends payload: "
710
+ "{\"DeviceCollection\":{\"total\":1,\"rows\":[{\"id\":\"<id>\"}]},"
711
+ "\"isResetWithoutNetwork\":false}\n"
712
+ "- Set 'is_reset_without_network' to true to reset without network."
713
+ ),
714
+ inputSchema={
715
+ "type": "object",
716
+ "properties": {
717
+ # ---- target resolution (consistent with other device tools) ----
718
+ "device_id": {"type": "integer", "description": "Target device id"},
719
+ "device_name": {"type": "string", "description": "Target device name"},
720
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
721
+ "door_id": {"type": "integer", "description": "Resolve device by door id (entry by default)"},
722
+ "door_name": {"type": "string", "description": "Resolve device by door name (substring)"},
723
+ "edge": {
724
+ "type": "string",
725
+ "enum": ["entry", "exit"],
726
+ "description": "When resolving by door, choose entry or exit device (default: entry)"
727
+ },
728
+
729
+ # ---- reset options ----
730
+ "is_reset_without_network": {
731
+ "type": "boolean",
732
+ "description": "If true, perform reset without network; default false"
733
+ },
734
+ "dry_run": {
735
+ "type": "boolean",
736
+ "description": "If true, only report what would happen without calling the reset API"
737
+ }
738
+ },
739
+ "anyOf": [
740
+ {"required": ["device_id"]},
741
+ {"required": ["device_name"]},
742
+ {"required": ["door_id"]},
743
+ {"required": ["door_name"]},
744
+ {"required": ["device_search_text"]}
745
+ ]
746
+ }
747
+ )
748
+
749
+ UPDATE_DEVICE_AUTH_DISPLAY_TOOL = Tool(
750
+ name="update-device-auth-and-display",
751
+ description=(
752
+ "Update a device's auth/display options in one shot. "
753
+ "Controls five fields: authentication.auth_timeout(3-20s), "
754
+ "authentication.enable_server_matching, authentication.enable_full_access, "
755
+ "display.display_userid_option, display.display_username_option."
756
+ ),
757
+ inputSchema={
758
+ "type": "object",
759
+ "properties": {
760
+ "device_id": {
761
+ "oneOf": [{"type": "integer"}, {"type": "string"}],
762
+ "description": "Target device id. If omitted, use device_name."
763
+ },
764
+ "device_name": {
765
+ "type": "string",
766
+ "description": "Exact device name when device_id is not provided."
767
+ },
768
+ "auth_timeout": {
769
+ "oneOf": [
770
+ {"type": "integer", "minimum": 3, "maximum": 20},
771
+ {"type": "string", "pattern": "^(?:[3-9]|1[0-9]|20)$"}
772
+ ],
773
+ "description": "Authentication wait time in seconds (3-20)."
774
+ },
775
+ "enable_server_matching": {"type": "boolean"},
776
+ "enable_full_access": {"type": "boolean"},
777
+ "display_userid_option": {
778
+ "oneOf": [
779
+ {"type": "integer", "enum": [0,1,2]},
780
+ {"type": "string", "enum": ["all","first","none"]}
781
+ ],
782
+ "description": "User ID display: 0=all, 1=first-only, 2=hidden"
783
+ },
784
+ "display_username_option": {
785
+ "oneOf": [
786
+ {"type": "integer", "enum": [0,1,2]},
787
+ {"type": "string", "enum": ["all","first","none"]}
788
+ ],
789
+ "description": "User name display: 0=all, 1=first-only, 2=hidden"
790
+ },
791
+ "dry_run": {"type": "boolean"}
792
+ },
793
+ "required": [],
794
+ "additionalProperties": False
795
+ }
796
+ )
797
+
798
+ BULK_ADD_DEVICES_TOOL = Tool(
799
+ name="bulk-add-devices",
800
+ description=(
801
+ "Add multiple devices by iterating the single 'add-device' logic sequentially.\n"
802
+ "- Input 'devices': array of objects with the SAME schema as 'add-device'.\n"
803
+ "- For each item, it calls the existing add-device implementation and aggregates results.\n"
804
+ "- Set 'continue_on_error' (default: true) to keep going after a failure."
805
+ ),
806
+ inputSchema={
807
+ "type": "object",
808
+ "properties": {
809
+ "devices": {
810
+ "type": "array",
811
+ "description": "List of device objects; each item uses the same fields as 'add-device'.",
812
+ "items": {
813
+ "type": "object",
814
+ "properties": {
815
+ "device_id": {"type": "integer", "description": "Unique device ID"},
816
+ "name": {"type": "string", "description": "Device name"},
817
+ "device_type_id": {"type": "integer", "description": "Optional device type id"},
818
+ "device_type_name": {"type": "string", "description": "Optional device type name"},
819
+ "ip_address": {"type": "string", "description": "IP address"},
820
+ "port": {"type": "integer", "description": "Port (default 51211)"},
821
+ "use_ssl": {"type": "boolean", "description": "Use SSL"},
822
+ "device_group_id":{"type": "integer", "description": "Target device group id"}
823
+ },
824
+ "required": ["device_id", "name", "ip_address", "device_group_id"]
825
+ }
826
+ },
827
+ "continue_on_error": {
828
+ "type": "boolean",
829
+ "default": True,
830
+ "description": "If false, stop at the first failure."
831
+ }
832
+ },
833
+ "required": ["devices"]
834
+ }
835
+ )
836
+
837
+ UPDATE_DEVICE_NETWORK_TOOL = Tool(
838
+ name="update-device-network",
839
+ description=(
840
+ "Update TCP/IP network settings of a device. "
841
+ "Maps the UI fields exactly: DHCP toggle, IP address, subnet mask, gateway, DNS server, and device port. "
842
+ "If dhcp_enabled=true, static fields (ip/subnet/gateway/dns) are ignored."
843
+ ),
844
+ inputSchema={
845
+ "type": "object",
846
+ "properties": {
847
+ # ---- Target resolution (same policy as other device tools) ----
848
+ "device_id": {"type": "integer", "description": "Target device id"},
849
+ "device_name": {"type": "string", "description": "Target device name"},
850
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
851
+ "door_id": {"type": "integer", "description": "Resolve via door id (entry by default)"},
852
+ "door_name": {"type": "string", "description": "Resolve via door name (substring)"},
853
+ "edge": {
854
+ "type": "string",
855
+ "enum": ["entry", "exit"],
856
+ "description": "When resolving by door, choose entry or exit device (default: entry)"
857
+ },
858
+
859
+ # ---- Network fields (mapped from the screenshot) ----
860
+ "dhcp_enabled": {"type": "boolean", "description": "When true, static fields are ignored and disabled."},
861
+ "ip_address": {
862
+ "type": "string",
863
+ "description": "IPv4 address (required when dhcp_enabled=false)",
864
+ "pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$"
865
+ },
866
+ "subnet_mask": {
867
+ "type": "string",
868
+ "description": "IPv4 subnet mask (required when dhcp_enabled=false)",
869
+ "pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$"
870
+ },
871
+ "gateway": {
872
+ "type": "string",
873
+ "description": "IPv4 gateway (optional when dhcp_enabled=false)",
874
+ "pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$"
875
+ },
876
+ "dns_server": {
877
+ "type": "string",
878
+ "description": "IPv4 DNS server (optional when dhcp_enabled=false)",
879
+ "pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$"
880
+ },
881
+ "device_port": {
882
+ "type": "integer",
883
+ "description": "Device port (default 51211)",
884
+ "minimum": 1,
885
+ "maximum": 65535,
886
+ "default": 51211
887
+ }
888
+ },
889
+ "allOf": [
890
+ # target resolution: require any one of the selectors
891
+ {
892
+ "anyOf": [
893
+ {"required": ["device_id"]},
894
+ {"required": ["device_name"]},
895
+ {"required": ["device_search_text"]},
896
+ {"required": ["door_id"]},
897
+ {"required": ["door_name"]}
898
+ ]
899
+ },
900
+ # DHCP gating: if dhcp_enabled=false, ip_address+subnet_mask required
901
+ {
902
+ "if": {
903
+ "properties": {"dhcp_enabled": {"const": False}},
904
+ "required": ["dhcp_enabled"]
905
+ },
906
+ "then": {
907
+ "required": ["ip_address", "subnet_mask"]
908
+ }
909
+ }
910
+ ],
911
+ "required": ["dhcp_enabled"]
912
+ }
913
+ )
914
+
915
+ UPDATE_DEVICE_SERIAL_COMM_TOOL = Tool(
916
+ name="update-device-serial-comm",
917
+ description=(
918
+ "Update RS-485 serial communication settings of a device via PUT /api/devices/{id}.\n"
919
+ "- Fields mirror the UI: rs485_mode (Master/Slave/DeviceDefault), baud_rate, show_osdp_result.\n"
920
+ "- If rs485_mode is 'master', 'show_osdp_result' must NOT be provided (UI hides it).\n"
921
+ "- If rs485_mode is 'slave' (and the model supports it), 'show_osdp_result' may be set:\n"
922
+ " - 0|'controller' -> Display controller's auth result\n"
923
+ " - 1|'device' -> Display device's own auth result\n"
924
+ "- All unspecified properties are preserved exactly as-is by the handler (merge patch)."
925
+ ),
926
+ inputSchema={
927
+ "type": "object",
928
+ "properties": {
929
+ # ---- Target resolution (consistent with other device tools) ----
930
+ "device_id": {"type": "integer", "description": "Target device id"},
931
+ "device_name": {"type": "string", "description": "Target device name (exact match or resolver policy)"},
932
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
933
+ "door_id": {"type": "integer", "description": "Resolve via door id (entry by default)"},
934
+ "door_name": {"type": "string", "description": "Resolve via door name (substring)"},
935
+ "edge": {
936
+ "type": "string",
937
+ "enum": ["entry", "exit"],
938
+ "description": "When resolving by door, choose entry or exit device (default: entry)"
939
+ },
940
+
941
+ # ---- Serial comm fields (mirroring the screenshots) ----
942
+ "rs485_mode": {
943
+ "oneOf": [
944
+ {"type": "integer", "enum": [1, 2, 3]},
945
+ {"type": "string", "enum": ["master", "slave", "device_default"]}
946
+ ],
947
+ "description": "RS-485 role: 1|master, 2|slave, 3|device_default (follow device default)."
948
+ },
949
+ "baud_rate": {
950
+ "type": "integer",
951
+ "enum": [9600, 19200, 38400, 57600, 115200],
952
+ "description": "RS-485 baud rate."
953
+ },
954
+ "show_osdp_result": {
955
+ "oneOf": [
956
+ {"type": "integer", "enum": [0, 1]},
957
+ {"type": "string", "enum": ["controller", "device"]}
958
+ ],
959
+ "description": (
960
+ "Only applicable when rs485_mode='slave'. "
961
+ "0|'controller' = show controller result, 1|'device' = show device result."
962
+ )
963
+ },
964
+
965
+ # ---- Optional dry-run (no PUT; return computed payload preview) ----
966
+ "dry_run": {
967
+ "type": "boolean",
968
+ "default": False,
969
+ "description": "If true, do not PUT; only resolve and return the computed payload."
970
+ }
971
+ },
972
+ # Require any one target selector
973
+ "allOf": [
974
+ {
975
+ "anyOf": [
976
+ {"required": ["device_id"]},
977
+ {"required": ["device_name"]},
978
+ {"required": ["device_search_text"]},
979
+ {"required": ["door_id"]},
980
+ {"required": ["door_name"]}
981
+ ]
982
+ },
983
+ # If rs485_mode is provided and indicates MASTER, 'show_osdp_result' must not be present
984
+ {
985
+ "if": {
986
+ "properties": {
987
+ "rs485_mode": {
988
+ "oneOf": [
989
+ {"const": 1},
990
+ {"const": "master"}
991
+ ]
992
+ }
993
+ },
994
+ "required": ["rs485_mode"]
995
+ },
996
+ "then": {
997
+ "not": {"required": ["show_osdp_result"]}
998
+ }
999
+ }
1000
+ ],
1001
+ "required": ["rs485_mode", "baud_rate"]
1002
+ }
1003
+ )
1004
+
1005
+ UPDATE_DEVICE_SERVER_COMM_TOOL = Tool(
1006
+ name="update-device-server-comm",
1007
+ description=(
1008
+ "Update a device's Server Communication settings via PUT /api/devices/{id}. "
1009
+ "Fields map 1:1 to the UI: [Connect to server from device], [Server address], [Server port]. "
1010
+ "Resolves target by id/name/door, merges minimal fields, preserves other fields."
1011
+ ),
1012
+ inputSchema={
1013
+ "type": "object",
1014
+ "properties": {
1015
+ # ---- Target resolution (same policy as other device tools) ----
1016
+ "device_id": {"type": "integer", "description": "Target device id"},
1017
+ "device_name": {"type": "string", "description": "Target device name (exact/contains per handler policy)"},
1018
+ "device_search_text": {"type": "string", "description": "Alternative substring search text"},
1019
+ "door_id": {"type": "integer", "description": "Resolve via door id"},
1020
+ "door_name": {"type": "string", "description": "Resolve via door name"},
1021
+ "edge": {
1022
+ "type": "string", "enum": ["entry", "exit"],
1023
+ "description": "When resolving via door, choose entry or exit (default: entry)"
1024
+ },
1025
+
1026
+ # ---- Server communication fields (UI mapping) ----
1027
+ "connect_from_device": {
1028
+ "type": "boolean",
1029
+ "description": "Checkbox 'Connect to server from device' (True=enable)"
1030
+ },
1031
+ "server_address": {
1032
+ "type": "string",
1033
+ "description": "Server address (IPv4 or hostname)"
1034
+ },
1035
+ "server_port": {
1036
+ "type": "integer",
1037
+ "minimum": 1, "maximum": 65535,
1038
+ "default": 51212,
1039
+ "description": "Server port (default 51212)"
1040
+ },
1041
+
1042
+ # ---- Dry-run support ----
1043
+ "dry_run": {
1044
+ "type": "boolean",
1045
+ "default": False,
1046
+ "description": "If true, do not PUT; return payload preview only"
1047
+ }
1048
+ },
1049
+ "allOf": [
1050
+ {
1051
+ "anyOf": [
1052
+ {"required": ["device_id"]},
1053
+ {"required": ["device_name"]},
1054
+ {"required": ["device_search_text"]},
1055
+ {"required": ["door_id"]},
1056
+ {"required": ["door_name"]}
1057
+ ]
1058
+ },
1059
+ {
1060
+ "anyOf": [
1061
+ {"required": ["connect_from_device"]},
1062
+ {"required": ["server_address"]},
1063
+ {"required": ["server_port"]}
1064
+ ]
1065
+ }
1066
+ ],
1067
+ "required": []
1068
+ }
1069
+ )
1070
+
1071
+ UPDATE_DEVICE_FINGERPRINT_TOOL = Tool(
1072
+ name="update-device-fingerprint",
1073
+ description=(
1074
+ "Update fingerprint-related settings via PUT /api/devices/{id}. "
1075
+ "Resolves target by device_id/device_name/device_search_text or door_id/door_name (edge=entry|exit). "
1076
+ "Accepts EN/KR enums, validates ranges, and sends a minimal merged payload for Device.fingerprint "
1077
+ "and Device.authentication.matching_timeout."
1078
+ ),
1079
+ inputSchema={
1080
+ "type": "object",
1081
+ "properties": {
1082
+ # ---- target resolution ----
1083
+ "device_id": {
1084
+ "oneOf": [{"type": "integer"}, {"type": "string"}],
1085
+ "description": "Device id"
1086
+ },
1087
+ "device_name": {
1088
+ "type": "string",
1089
+ "description": "Device name (exact or substring depending on resolver)"
1090
+ },
1091
+ "device_search_text": {
1092
+ "type": "string",
1093
+ "description": "Alternate device query (substring)"
1094
+ },
1095
+ "door_id": {
1096
+ "type": "integer",
1097
+ "description": "Resolve by door id (entry by default unless edge is specified)"
1098
+ },
1099
+ "door_name": {
1100
+ "type": "string",
1101
+ "description": "Resolve by door name (substring)"
1102
+ },
1103
+ "edge": {
1104
+ "type": "string",
1105
+ "enum": ["entry", "exit"],
1106
+ "description": "Edge selector when resolving by door"
1107
+ },
1108
+
1109
+ # ---- fingerprint options ----
1110
+ "security_level": {
1111
+ "oneOf": [
1112
+ {"type": "integer", "enum": [0, 1, 2]},
1113
+ {"type": "string", "enum": ["normal", "secure", "most secure", "high", "highest"]}
1114
+ ],
1115
+ "description": "Fingerprint 1:N security level (0=normal, 1=secure, 2=most secure) -> fingerprint.security_level"
1116
+ },
1117
+ "fast_mode": {
1118
+ "oneOf": [
1119
+ {"type": "integer", "enum": [0, 1, 2, 3]},
1120
+ {"type": "string", "enum": ["auto", "normal", "fast", "fastest"]}
1121
+ ],
1122
+ "description": "Fingerprint Fast Mode (0=auto, 1=normal, 2=fast, 3=fastest) -> fingerprint.fast_mode"
1123
+ },
1124
+ "sensitivity": {
1125
+ "type": "integer",
1126
+ "minimum": 1,
1127
+ "maximum": 7,
1128
+ "description": "Sensor sensitivity (1..7) -> fingerprint.sensitivity"
1129
+ },
1130
+ "scan_wait_time": {
1131
+ "type": "integer",
1132
+ "minimum": 1,
1133
+ "maximum": 20,
1134
+ "description": "Scan wait time in seconds (1..20) -> fingerprint.scan_timeout"
1135
+ },
1136
+ "matching_wait_time": {
1137
+ "type": "integer",
1138
+ "minimum": 1,
1139
+ "maximum": 20,
1140
+ "description": "Matching wait time in seconds (1..20) -> authentication.matching_timeout"
1141
+ },
1142
+ "show_image": {
1143
+ "type": "boolean",
1144
+ "description": "Display fingerprint image -> fingerprint.show_image"
1145
+ },
1146
+ "sensor_mode": {
1147
+ "oneOf": [
1148
+ {"type": "integer", "enum": [0, 1]},
1149
+ {"type": "string", "enum": ["auto on", "always on", "auto_on", "always_on", "auto", "always"]}
1150
+ ],
1151
+ "description": "Sensor Mode (0=Auto On, 1=Always On) -> fingerprint.sensor_mode"
1152
+ },
1153
+ "advanced_enrollment": {
1154
+ "type": "boolean",
1155
+ "description": "Enrollment quality check -> fingerprint.advanced_enrollment"
1156
+ },
1157
+ "lfd_level": {
1158
+ "oneOf": [
1159
+ {"type": "integer", "enum": [0, 1, 2, 3]},
1160
+ {"type": "string", "enum": ["off", "normal", "strong", "very strong", "very_strong"]}
1161
+ ],
1162
+ "description": "Fingerprint LFD level (0=Off, 1=Normal, 2=Strong, 3=Very Strong) -> fingerprint.lfd_level"
1163
+ },
1164
+ "duplicate_check": {
1165
+ "type": "boolean",
1166
+ "description": "Duplicate check -> fingerprint.duplicate_check"
1167
+ },
1168
+
1169
+ "dry_run": {
1170
+ "type": "boolean",
1171
+ "default": False,
1172
+ "description": "If true, do not PUT; return payload preview only"
1173
+ }
1174
+ },
1175
+ "allOf": [
1176
+ {
1177
+ "anyOf": [
1178
+ {"required": ["device_id"]},
1179
+ {"required": ["device_name"]},
1180
+ {"required": ["device_search_text"]},
1181
+ {"required": ["door_id"]},
1182
+ {"required": ["door_name"]}
1183
+ ]
1184
+ }
1185
+ ],
1186
+ "additionalProperties": False
1187
+ }
1188
+ )
1189
+
1190
+ UPDATE_DEVICE_FACE_TOOL = Tool(
1191
+ name="update-device-face",
1192
+ description=(
1193
+ "Update face-related settings via PUT /api/devices/{id}.\n"
1194
+ "- Always resolve device_name/device_search_text to device_id first (never PUT by name).\n"
1195
+ "- English-only enums/toggles; booleans are real booleans; enums/values are integers.\n"
1196
+ "- Enrollment time is 10..20 seconds (step 1, no snapping).\n"
1197
+ "- Max head pose angle must be one of {15,30,45,60,75,90}.\n"
1198
+ "- Detection distances use 10-step values; max also accepts 'over_130' -> 255.\n"
1199
+ "- Sends a minimal payload: {\"Device\": {\"face\": { ... }}} only."
1200
+ ),
1201
+ inputSchema={
1202
+ "type": "object",
1203
+ "properties": {
1204
+ "device_id": {"oneOf": [{"type": "integer"}, {"type": "string"}], "description": "Device id"},
1205
+ "device_name": {"type": "string", "description": "Device name (resolved to id internally)"},
1206
+ "device_search_text": {"type": "string", "description": "Alternate device query (resolved to id internally)"},
1207
+ "door_id": {"type": "integer", "description": "Optional: resolve by door (fallback uses generic resolver)"},
1208
+ "door_name": {"type": "string", "description": "Optional: resolve by door (fallback uses generic resolver)"},
1209
+ "edge": {"type": "string", "enum": ["entry","exit"], "description": "Edge selector for door resolution"},
1210
+ "security_level": {
1211
+ "oneOf": [
1212
+ {"type": "integer", "enum": [0,1,2]},
1213
+ {"type": "string", "enum": ["normal","secure","most secure","most_secure"]}
1214
+ ],
1215
+ "description": "-> face.security_level"
1216
+ },
1217
+ "motion_sensor": {
1218
+ "oneOf": [
1219
+ {"type": "integer", "enum": [0,1,2,3]},
1220
+ {"type": "string", "enum": ["off","low","medium","high"]}
1221
+ ],
1222
+ "description": "-> face.sensitivity"
1223
+ },
1224
+ "sensitivity": { # alias
1225
+ "oneOf": [
1226
+ {"type": "integer", "enum": [0,1,2,3]},
1227
+ {"type": "string", "enum": ["off","low","medium","high"]}
1228
+ ],
1229
+ "description": "[Alias of motion_sensor] -> face.sensitivity"
1230
+ },
1231
+ "lfd_level": {
1232
+ "oneOf": [
1233
+ {"type": "integer", "enum": [0,1,2,3]},
1234
+ {"type": "string", "enum": ["normal","secure","more secure","most secure","more_secure","most_secure"]}
1235
+ ],
1236
+ "description": "-> face.lfd_level"
1237
+ },
1238
+ "enroll_timeout": {"type": "integer", "minimum": 10, "maximum": 20, "description": "-> face.scan_wait_time"},
1239
+ "max_rotation": {"type": "integer", "enum": [15,30,45,60,75,90], "description": "-> face.max_rotation"},
1240
+ "detect_distance_min": {"type": "integer", "enum": [30,40,50,60,70,80,90,100,110,120,130], "description": "-> face.detection_distance_min"},
1241
+ "detect_distance_max": {
1242
+ "oneOf": [
1243
+ {"type": "integer", "enum": [30,40,50,60,70,80,90,100,110,120,130]},
1244
+ {"type": "integer", "enum": [255]},
1245
+ {"type": "string", "enum": ["over_130","above_130","over-130","130_plus","130plus"]}
1246
+ ],
1247
+ "description": "-> face.detection_distance_max"
1248
+ },
1249
+ "face_width_min": {"type": "integer", "minimum": 0, "maximum": 100, "description": "-> face.face_width_min"},
1250
+ "face_width_max": {"type": "integer", "minimum": 0, "maximum": 100, "description": "-> face.face_width_max"},
1251
+ "operation_mode": {
1252
+ "oneOf": [
1253
+ {"type": "integer", "enum": [0,1]},
1254
+ {"type": "string", "enum": ["normal","fusion"]}
1255
+ ],
1256
+ "description": "-> face.operation_mode"
1257
+ },
1258
+ "wide_search": {"type": "boolean", "description": "-> face.face_wide_search"},
1259
+ "fast_enroll": {"type": "boolean", "description": "-> face.quick_enrollment"},
1260
+ "save_image": {"type": "boolean", "description": "-> face.store_visual_face_image"},
1261
+ "duplicate_check": {"type": "boolean", "description": "-> face.duplicate_check"},
1262
+ "light_brightness": {
1263
+ "oneOf": [
1264
+ {"type": "integer", "enum": [0,1,3]},
1265
+ {"type": "string", "enum": ["normal","high","off","not use","not_use"]}
1266
+ ],
1267
+ "description": "Mapped to face.proximity_level (0/1/3 only; 2 is unused)"
1268
+ },
1269
+ "dry_run": {"type": "boolean", "default": False, "description": "If true, do not PUT; return payload preview only"}
1270
+ },
1271
+ "allOf": [{
1272
+ "anyOf": [
1273
+ {"required": ["device_id"]},
1274
+ {"required": ["device_name"]},
1275
+ {"required": ["device_search_text"]},
1276
+ {"required": ["door_id"]},
1277
+ {"required": ["door_name"]}
1278
+ ]
1279
+ }],
1280
+ "additionalProperties": False
1281
+ }
1282
+ )
1283
+
1284
+ WIEGAND_CSN_FORMATS = {
1285
+ "26 bit SIA Standard-H10301": "0",
1286
+ "HID 37 bit-H10302": "1",
1287
+ "HID 37 bit-H10304": "2",
1288
+ "HID Corporate 1000": "3",
1289
+ "HID Corporate 1000 48bit": "4",
1290
+ "MIFARE CSN 32bit": "5",
1291
+ "MIFARE CSN 34bit (Parity)": "6",
1292
+ "DESFire 56bit": "7",
1293
+ "DESFire 58bit (Parity)": "8",
1294
+ # ids 9..14 are empty in your capture (reserved/unused)
1295
+ }
1296
+
1297
+
1298
+ UPDATE_DEVICE_CARD_CSN_TOOL = Tool(
1299
+ name="update-device-card-csn",
1300
+ description=(
1301
+ "Update only the 'Card Type > CSN Card' fields via PUT /api/devices/{id}. "
1302
+ "Minimal payload: patches Device.card (and optionally Device.wiegand.wiegand_csn_id when format_type='wiegand'). "
1303
+ "Mappings: csn_enabled->card.use_csn, em4100_enabled->card.use_em, mifare_felica_enabled->card.use_mifare_felica, "
1304
+ "byte_order->card.byte_order (LSB='1', MSB='0'), format_type->card.use_wiegand_format ('wiegand'='true', 'normal'='false'). "
1305
+ "If a property is omitted, it will not be updated. "
1306
+ "Static Wiegand name->id map is embedded (e.g., 'DESFire 56bit' -> '7'). "
1307
+ "Runtime rule: If any CSN sub-option is enabled or a Wiegand format is selected, the tool will auto-set csn_enabled=true. "
1308
+ "Also, when csn_enabled=true and neither EM4100 nor MIFARE/Felica is chosen, both will be auto-enabled."
1309
+ ),
1310
+ inputSchema={
1311
+ "type": "object",
1312
+ "properties": {
1313
+ "device_id": {"type": "integer", "description": "Target device id"},
1314
+ "device_name": {"type": "string", "description": "Target device name (resolver-dependent)"},
1315
+ "device_search_text": {"type": "string", "description": "Alternate device query (substring)"},
1316
+ "door_id": {"type": "integer", "description": "Resolve via door id (entry by default)"},
1317
+ "door_name": {"type": "string", "description": "Resolve via door name (substring)"},
1318
+ "edge": {"type": "string", "enum": ["entry", "exit"], "description": "Door edge selector when resolving by door"},
1319
+
1320
+ # CSN-only fields
1321
+ "csn_enabled": {"type": "boolean", "description": "Map to Device.card.use_csn (required)"},
1322
+ "em4100_enabled": {"type": "boolean", "description": "Map to Device.card.use_em"},
1323
+ "mifare_felica_enabled": {"type": "boolean", "description": "Map to Device.card.use_mifare_felica"},
1324
+
1325
+ # Byte order: LSB -> '1', MSB -> '0'
1326
+ "byte_order": {
1327
+ "oneOf": [
1328
+ {"type": "string", "enum": ["LSB", "MSB", "lsb", "msb", "0", "1"]},
1329
+ {"type": "integer", "enum": [0, 1]}
1330
+ ],
1331
+ "description": "Map to Device.card.byte_order (LSB='1', MSB='0')."
1332
+ },
1333
+
1334
+ # Format type toggle (does NOT pick a format id unless provided)
1335
+ "format_type": {
1336
+ "type": "string",
1337
+ "enum": ["normal", "wiegand"],
1338
+ "description": "Map to Device.card.use_wiegand_format"
1339
+ },
1340
+
1341
+ # Human-friendly Wiegand name
1342
+ "wiegand_format": {
1343
+ "type": "string",
1344
+ "enum": [
1345
+ "26 bit SIA Standard-H10301",
1346
+ "HID 37 bit-H10302",
1347
+ "HID 37 bit-H10304",
1348
+ "HID Corporate 1000",
1349
+ "HID Corporate 1000 48bit",
1350
+ "MIFARE CSN 32bit",
1351
+ "MIFARE CSN 34bit (Parity)",
1352
+ "DESFire 56bit",
1353
+ "DESFire 58bit (Parity)"
1354
+ ],
1355
+ "description": (
1356
+ "Human-friendly Wiegand CSN name. Will be translated to id using a static map. "
1357
+ "If both wiegand_format and wiegand_format_id are given, they must match."
1358
+ )
1359
+ },
1360
+
1361
+ # Wiegand CSN format id
1362
+ "wiegand_format_id": {
1363
+ "oneOf": [
1364
+ {"type": "integer"},
1365
+ {"type": "string", "pattern": "^-?\\d+$"}
1366
+ ],
1367
+ "description": "Map to Device.wiegand.wiegand_csn_id.id (e.g., 7 for 'DESFire 56bit')."
1368
+ }
1369
+ },
1370
+
1371
+ "required": ["csn_enabled"],
1372
+
1373
+ "allOf": [
1374
+ # 1) Require at least one target selector (unchanged)
1375
+ {
1376
+ "anyOf": [
1377
+ {"required": ["device_id"]},
1378
+ {"required": ["device_name"]},
1379
+ {"required": ["device_search_text"]},
1380
+ {"required": ["door_id"]},
1381
+ {"required": ["door_name"]}
1382
+ ]
1383
+ },
1384
+
1385
+ # 2) If format_type='normal' -> do NOT allow Wiegand selection (unchanged)
1386
+ {
1387
+ "if": {"properties": {"format_type": {"const": "normal"}}, "required": ["format_type"]},
1388
+ "then": {
1389
+ "not": {
1390
+ "anyOf": [
1391
+ {"required": ["wiegand_format_id"]},
1392
+ {"required": ["wiegand_format"]}
1393
+ ]
1394
+ }
1395
+ }
1396
+ }
1397
+ ],
1398
+
1399
+ "additionalProperties": False
1400
+ }
1401
+ )
1402
+
1403
+ LIST_WAITING_DEVICES_TOOL = Tool(
1404
+ name="list-waiting-devices",
1405
+ description=(
1406
+ "List devices waiting to be added to BioStar 2 server. "
1407
+ "When devices connect directly to the server (not via TCP/UDP search), "
1408
+ "they appear in the waiting list. Use this to discover devices before adding them. "
1409
+ "GET /api/devices/waiting returns devices with connection_mode='1' (Server -> Device)."
1410
+ ),
1411
+ inputSchema={
1412
+ "type": "object",
1413
+ "properties": {
1414
+ "ip_filter": {
1415
+ "type": "string",
1416
+ "description": "Filter devices by IP address (substring match)"
1417
+ },
1418
+ "device_type_id": {
1419
+ "type": "integer",
1420
+ "description": "Filter by device type ID (e.g., 35 for BioStation 3)"
1421
+ }
1422
+ },
1423
+ "required": []
1424
+ }
1425
+ )
1426
+
1427
+ ADD_WAITING_DEVICE_TOOL = Tool(
1428
+ name="add-waiting-device",
1429
+ description=(
1430
+ "Add a device from the waiting list to BioStar 2 server. "
1431
+ "This is an alternative to tcp-search/add-device. "
1432
+ "The device must already be in the waiting list (check with list-waiting-devices first). "
1433
+ "POST /api/devices with waiting device details."
1434
+ ),
1435
+ inputSchema={
1436
+ "type": "object",
1437
+ "properties": {
1438
+ "waiting_device_id": {
1439
+ "oneOf": [{"type": "integer"}, {"type": "string"}],
1440
+ "description": "Device ID from the waiting list (required)"
1441
+ },
1442
+ "name": {
1443
+ "type": "string",
1444
+ "description": "Name for the device. If not provided, auto-generates from device type and ID"
1445
+ },
1446
+ "device_type_id": {
1447
+ "type": "integer",
1448
+ "description": "Device type ID (usually obtained from waiting device info)"
1449
+ },
1450
+ "ip_address": {
1451
+ "type": "string",
1452
+ "description": "IP address (from waiting device info)"
1453
+ },
1454
+ "device_group_id": {
1455
+ "type": "integer",
1456
+ "description": "Device group ID to assign (default: 1 for root group)",
1457
+ "default": 1
1458
+ },
1459
+ "use_alphanumeric": {
1460
+ "type": "boolean",
1461
+ "description": "Enable alphanumeric user IDs",
1462
+ "default": True
1463
+ },
1464
+ "follower_server_id": {
1465
+ "oneOf": [{"type": "integer"}, {"type": "string"}],
1466
+ "description": "Follower server ID (default: 1)",
1467
+ "default": 1
1468
+ }
1469
+ },
1470
+ "required": ["waiting_device_id"]
1471
+ }
1472
+ )
1473
+
1474
+ BULK_ADD_WAITING_DEVICES_TOOL = Tool(
1475
+ name="bulk-add-waiting-devices",
1476
+ description=(
1477
+ "Add multiple devices from the waiting list in bulk. "
1478
+ "Each device must be in the waiting list. Use list-waiting-devices first to get device IDs. "
1479
+ "Iterates through each device and calls add-waiting-device logic."
1480
+ ),
1481
+ inputSchema={
1482
+ "type": "object",
1483
+ "properties": {
1484
+ "waiting_device_ids": {
1485
+ "type": "array",
1486
+ "items": {"oneOf": [{"type": "integer"}, {"type": "string"}]},
1487
+ "description": "Array of waiting device IDs to add"
1488
+ },
1489
+ "device_group_id": {
1490
+ "type": "integer",
1491
+ "description": "Device group ID for all devices (default: 1)",
1492
+ "default": 1
1493
+ },
1494
+ "name_prefix": {
1495
+ "type": "string",
1496
+ "description": "Optional prefix for auto-generated device names"
1497
+ },
1498
+ "use_alphanumeric": {
1499
+ "type": "boolean",
1500
+ "description": "Enable alphanumeric user IDs for all devices",
1501
+ "default": True
1502
+ },
1503
+ "continue_on_error": {
1504
+ "type": "boolean",
1505
+ "description": "Continue adding remaining devices if one fails",
1506
+ "default": True
1507
+ }
1508
+ },
1509
+ "required": ["waiting_device_ids"]
1510
+ }
1511
+ )
1512
+
1513
+ DEVICE_TOOLS = [
1514
+ LIST_DEVICES_TOOL,
1515
+ GET_DEVICE_TOOL,
1516
+ ADD_DEVICE_TOOL,
1517
+ UPDATE_DEVICE_TOOL,
1518
+ REBOOT_DEVICE_TOOL,
1519
+ GET_DEVICE_STATUS_TOOL,
1520
+ SCAN_DEVICES_TOOL,
1521
+ TCP_SEARCH_TOOL,
1522
+ UDP_SEARCH_TOOL,
1523
+ DISCONNECT_DEVICE_TOOL,
1524
+ REMOVE_DEVICE_TOOL,
1525
+ SYNC_DEVICE_TIME_TOOL,
1526
+ CLEAR_DEVICE_LOG_TOOL,
1527
+ LOCK_DEVICE_TOOL,
1528
+ UNLOCK_DEVICE_TOOL,
1529
+ GET_DEVICE_GROUPS_TOOL,
1530
+ GET_DEVICE_GROUP_TOOL,
1531
+ CREATE_DEVICE_GROUP_TOOL,
1532
+ UPDATE_DEVICE_GROUP_TOOL,
1533
+ DELETE_DEVICE_GROUP_TOOL,
1534
+ MOVE_DEVICE_TO_GROUP_TOOL,
1535
+ REMOVE_DEVICE_FROM_GROUP_TOOL,
1536
+ UPDATE_DEVICE_AUTH_MODE_TOOL,
1537
+ UPDATE_DEVICE_FACE_DISTANCE_TOOL,
1538
+ UPDATE_DEVICE_DATE_TYPE_TOOL,
1539
+ UPDATE_DEVICE_TIMEZONE_TOOL,
1540
+ FACTORY_RESET_DEVICE_TOOL,
1541
+ BULK_ADD_DEVICES_TOOL,
1542
+ UPDATE_DEVICE_AUTH_DISPLAY_TOOL,
1543
+ UPDATE_DEVICE_NETWORK_TOOL,
1544
+ UPDATE_DEVICE_SERIAL_COMM_TOOL,
1545
+ UPDATE_DEVICE_SERVER_COMM_TOOL,
1546
+ UPDATE_DEVICE_FINGERPRINT_TOOL,
1547
+ UPDATE_DEVICE_FACE_TOOL,
1548
+ UPDATE_DEVICE_CARD_CSN_TOOL,
1549
+ LIST_WAITING_DEVICES_TOOL,
1550
+ ADD_WAITING_DEVICE_TOOL,
1551
+ BULK_ADD_WAITING_DEVICES_TOOL
1552
+ ]