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,221 @@
1
+ """
2
+ BioStar X Session Management
3
+ Handles API authentication and session lifecycle
4
+ """
5
+ import asyncio
6
+ import logging
7
+ from datetime import datetime
8
+ from typing import Optional, Dict, Any
9
+ import httpx
10
+ from .config import Config
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class BioStarSession:
16
+ """Manages BioStar X API session and authentication."""
17
+
18
+ def __init__(self, config: Config):
19
+ self.config = config
20
+ self.client: Optional[httpx.AsyncClient] = None
21
+ self.session_id: Optional[str] = None
22
+ self.user_id: Optional[str] = None
23
+ self.last_refresh: Optional[datetime] = None
24
+ self.timezone_offset_minutes: Optional[int] = None
25
+ self._lock = asyncio.Lock()
26
+
27
+ async def __aenter__(self):
28
+ await self.connect()
29
+ return self
30
+
31
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
32
+ await self.disconnect()
33
+
34
+ async def connect(self):
35
+ """Initialize HTTP client."""
36
+ if not self.client:
37
+ logger.info(f"Connecting to BioStar server at: {self.config.biostar_url}")
38
+ logger.info(f"SSL verification: {self.config.verify_ssl}, Timeout: {self.config.api_timeout}s")
39
+
40
+ self.client = httpx.AsyncClient(
41
+ base_url=self.config.biostar_url,
42
+ timeout=httpx.Timeout(self.config.api_timeout),
43
+ verify=self.config.verify_ssl,
44
+ headers={
45
+ "Content-Type": "application/json",
46
+ "Accept": "application/json"
47
+ }
48
+ )
49
+
50
+ async def set_session_id_async(self, session_id: str):
51
+ """Set session ID from external source (async version)."""
52
+ logger.info(f"Setting external session ID: {session_id}")
53
+ self.session_id = session_id
54
+ self.last_refresh = datetime.now()
55
+
56
+ if not self.client:
57
+ await self.connect()
58
+ logger.info("Client initialized for external session")
59
+
60
+ if self.client and self.session_id:
61
+ self.client.headers["bs-session-id"] = self.session_id
62
+ logger.info(f"Updated client headers with session ID: {session_id[:8]}...")
63
+
64
+ def set_session_id(self, session_id: str):
65
+ """Set session ID from external source (sync version)."""
66
+ logger.info(f"Setting external session ID: {session_id}")
67
+ self.session_id = session_id
68
+ self.last_refresh = datetime.now()
69
+
70
+ if self.client and self.session_id:
71
+ self.client.headers["bs-session-id"] = self.session_id
72
+ logger.info("Updated client headers with session ID")
73
+
74
+ async def disconnect(self):
75
+ """Close HTTP client and logout."""
76
+ if self.session_id:
77
+ try:
78
+ await self.logout()
79
+ except Exception as e:
80
+ logger.error(f"Error during logout: {e}")
81
+
82
+ if self.client:
83
+ await self.client.aclose()
84
+ self.client = None
85
+
86
+ async def login(self, username: Optional[str] = None, password: Optional[str] = None) -> Dict[str, Any]:
87
+ """Login to BioStar X API."""
88
+ async with self._lock:
89
+ if not self.client:
90
+ await self.connect()
91
+
92
+ login_username = username or self.config.biostar_username
93
+ login_password = password or self.config.biostar_password
94
+
95
+ if not login_username or not login_password:
96
+ raise ValueError("Username and password are required. Set them in .env or provide as arguments.")
97
+
98
+ try:
99
+ response = await self.client.post(
100
+ "/api/login",
101
+ json={
102
+ "User": {
103
+ "login_id": login_username,
104
+ "password": login_password
105
+ }
106
+ }
107
+ )
108
+ response.raise_for_status()
109
+
110
+ data = response.json()
111
+ self.session_id = response.headers.get("bs-session-id")
112
+ self.user_id = data.get("User", {}).get("user_id")
113
+ self.last_refresh = datetime.now()
114
+
115
+ if self.session_id:
116
+ self.client.headers["bs-session-id"] = self.session_id
117
+
118
+ logger.info(f"Successfully logged in as user {self.user_id}")
119
+ return data
120
+
121
+ except httpx.HTTPStatusError as e:
122
+ logger.error(f"Login failed: {e.response.status_code} - {e.response.text}")
123
+ raise
124
+ except Exception as e:
125
+ logger.error(f"Login error: {e}")
126
+ raise
127
+
128
+ async def logout(self) -> None:
129
+ """Logout from BioStar X API."""
130
+ if not self.session_id:
131
+ return
132
+
133
+ try:
134
+ await self.client.post("/api/logout")
135
+ logger.info("Successfully logged out")
136
+ except Exception as e:
137
+ logger.error(f"Logout error: {e}")
138
+ finally:
139
+ self.session_id = None
140
+ self.user_id = None
141
+ self.last_refresh = None
142
+ self.timezone_offset_minutes = None
143
+ if self.client:
144
+ self.client.headers.pop("bs-session-id", None)
145
+
146
+ async def refresh_session(self) -> None:
147
+ """Refresh session if needed."""
148
+ if not self.session_id:
149
+ await self.login()
150
+ return
151
+
152
+ if self.last_refresh:
153
+ elapsed = datetime.now() - self.last_refresh
154
+ if elapsed.total_seconds() > self.config.session_refresh_interval:
155
+ logger.info("Session refresh needed")
156
+ await self.login()
157
+
158
+ async def request(
159
+ self,
160
+ method: str,
161
+ endpoint: str,
162
+ json: Optional[Dict[str, Any]] = None,
163
+ params: Optional[Dict[str, Any]] = None,
164
+ **kwargs
165
+ ) -> httpx.Response:
166
+ """Make authenticated request to BioStar X API."""
167
+ if not self.client:
168
+ await self.connect()
169
+ logger.info("Client initialized on first request")
170
+
171
+ if self.session_id:
172
+ self.client.headers["bs-session-id"] = self.session_id
173
+ logger.info(f"Session headers set on first request: {self.session_id[:8]}...")
174
+
175
+ await self.refresh_session()
176
+
177
+ try:
178
+ response = await self.client.request(
179
+ method=method,
180
+ url=endpoint,
181
+ json=json,
182
+ params=params,
183
+ **kwargs
184
+ )
185
+ response.raise_for_status()
186
+ return response
187
+
188
+ except httpx.HTTPStatusError as e:
189
+ if e.response.status_code == 401:
190
+ logger.info("Session expired, re-authenticating")
191
+ await self.login()
192
+ response = await self.client.request(
193
+ method=method,
194
+ url=endpoint,
195
+ json=json,
196
+ params=params,
197
+ **kwargs
198
+ )
199
+ response.raise_for_status()
200
+ return response
201
+ raise
202
+
203
+ async def get(self, endpoint: str, **kwargs) -> httpx.Response:
204
+ """Make GET request."""
205
+ return await self.request("GET", endpoint, **kwargs)
206
+
207
+ async def post(self, endpoint: str, json: Dict[str, Any], **kwargs) -> httpx.Response:
208
+ """Make POST request."""
209
+ return await self.request("POST", endpoint, json=json, **kwargs)
210
+
211
+ async def put(self, endpoint: str, json: Dict[str, Any], **kwargs) -> httpx.Response:
212
+ """Make PUT request."""
213
+ return await self.request("PUT", endpoint, json=json, **kwargs)
214
+
215
+ async def delete(self, endpoint: str, **kwargs) -> httpx.Response:
216
+ """Make DELETE request."""
217
+ return await self.request("DELETE", endpoint, **kwargs)
218
+
219
+ def is_authenticated(self) -> bool:
220
+ """Check if session is authenticated."""
221
+ return self.session_id is not None
@@ -0,0 +1,172 @@
1
+ """
2
+ BioStar X MCP Server Tool Manager
3
+ Dynamic loading and management of tool categories
4
+ """
5
+ import logging
6
+ from typing import List, Set, Optional, Dict
7
+ from mcp.types import Tool
8
+
9
+ from .tools.categories import (
10
+ get_tools_for_category,
11
+ get_category_for_tool,
12
+ get_all_categories,
13
+ CATEGORY_DESCRIPTIONS
14
+ )
15
+ from .tools.auth import AUTH_TOOLS
16
+ from .tools.users import USER_TOOLS
17
+ from .tools.events import EVENT_TOOLS
18
+ from .tools.doors import DOOR_TOOLS
19
+ from .tools.devices import DEVICE_TOOLS
20
+ from .tools.access import ACCESS_TOOLS
21
+ from .tools.audit import AUDIT_TOOLS
22
+ from .tools.cards import CARD_TOOLS
23
+ from .tools.navigation import NAVIGATION_TOOLS
24
+ from .tools.occupancy import OCCUPANCY_TOOLS
25
+ from .tools.files import FILE_TOOLS
26
+ from .tools.logs import LOG_TOOLS
27
+ from .tools.help import HELP_TOOLS
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class ToolManager:
33
+ """Manages dynamic loading and unloading of tool categories."""
34
+
35
+ def __init__(self, max_categories: int = 15):
36
+ self.max_categories = max_categories
37
+ self.active_categories: Set[str] = set()
38
+ self.loaded_tools: Dict[str, List[Tool]] = {}
39
+ self.usage_count: Dict[str, int] = {}
40
+
41
+ def load_category(self, category: str) -> bool:
42
+ """Load tools for a specific category."""
43
+ logger.debug(f"Attempting to load category: {category}")
44
+
45
+ if category in self.active_categories:
46
+ logger.debug(f"Category {category} already loaded")
47
+ return True
48
+
49
+ if category not in get_all_categories():
50
+ logger.warning(f"Unknown category: {category}")
51
+ return False
52
+
53
+ # Check if we need to unload a category
54
+ if len(self.active_categories) >= self.max_categories:
55
+ logger.debug(f"Max categories reached ({self.max_categories}), unloading least used")
56
+ self._unload_least_used_category()
57
+
58
+ # Load the tools for this category
59
+ tools = self._get_tools_for_category(category)
60
+ logger.debug(f"Retrieved {len(tools)} tools for category: {category}")
61
+
62
+ if tools:
63
+ self.loaded_tools[category] = tools
64
+ self.active_categories.add(category)
65
+ self.usage_count[category] = 0
66
+ logger.info(f"Loaded category: {category} with {len(tools)} tools")
67
+ return True
68
+ else:
69
+ logger.error(f"No tools found for category: {category}")
70
+ return False
71
+
72
+ def unload_category(self, category: str) -> bool:
73
+ """Unload tools for a specific category."""
74
+ if category not in self.active_categories:
75
+ return False
76
+
77
+ self.active_categories.remove(category)
78
+ self.loaded_tools.pop(category, None)
79
+ self.usage_count.pop(category, None)
80
+ logger.info(f"Unloaded category: {category}")
81
+ return True
82
+
83
+ def get_active_tools(self) -> List[Tool]:
84
+ """Get all currently loaded tools."""
85
+ tools = []
86
+ seen_names = set()
87
+
88
+ for category in self.active_categories:
89
+ category_tools = self.loaded_tools.get(category, [])
90
+ for tool in category_tools:
91
+ tool_name = tool.name
92
+ if tool_name not in seen_names:
93
+ seen_names.add(tool_name)
94
+ tools.append(tool)
95
+ else:
96
+ logger.warning(f"Duplicate tool name found: {tool_name} from category {category}, skipping")
97
+
98
+ logger.info(f"Returning {len(tools)} unique tools from {len(self.active_categories)} categories")
99
+ return tools
100
+
101
+ def get_tool_category(self, tool_name: str) -> Optional[str]:
102
+ """Get the category a tool belongs to."""
103
+ return get_category_for_tool(tool_name)
104
+
105
+ def record_tool_usage(self, tool_name: str) -> None:
106
+ """Record that a tool was used."""
107
+ category = self.get_tool_category(tool_name)
108
+ if category and category in self.usage_count:
109
+ self.usage_count[category] += 1
110
+
111
+ def _unload_least_used_category(self) -> None:
112
+ """Unload the least recently used category."""
113
+ if not self.active_categories:
114
+ return
115
+
116
+ # Never unload auth category
117
+ candidates = [c for c in self.active_categories if c != "auth"]
118
+ if not candidates:
119
+ return
120
+
121
+ # Find least used category
122
+ least_used = min(candidates, key=lambda c: self.usage_count.get(c, 0))
123
+ self.unload_category(least_used)
124
+
125
+ # Category to tools mapping (without vector_solis)
126
+ _CATEGORY_TOOLS_MAP = {
127
+ "auth": AUTH_TOOLS,
128
+ "users": USER_TOOLS,
129
+ "events": EVENT_TOOLS,
130
+ "doors": DOOR_TOOLS,
131
+ "access": ACCESS_TOOLS,
132
+ "devices": DEVICE_TOOLS,
133
+ "audit": AUDIT_TOOLS,
134
+ "cards": CARD_TOOLS,
135
+ "navigation": NAVIGATION_TOOLS,
136
+ "occupancy": OCCUPANCY_TOOLS,
137
+ "files": FILE_TOOLS,
138
+ "logs": LOG_TOOLS,
139
+ "help": HELP_TOOLS,
140
+ }
141
+
142
+ def _get_tools_for_category(self, category: str) -> List[Tool]:
143
+ """Get tool definitions for a category."""
144
+ logger.debug(f"Getting tools for category: {category}")
145
+
146
+ tools = self._CATEGORY_TOOLS_MAP.get(category)
147
+
148
+ if tools is None:
149
+ logger.warning(f"Unknown category: {category}")
150
+ return []
151
+
152
+ return tools
153
+
154
+ def get_category_info(self) -> Dict[str, Dict[str, any]]:
155
+ """Get information about all categories."""
156
+ info = {}
157
+ for category in get_all_categories():
158
+ info[category] = {
159
+ "description": CATEGORY_DESCRIPTIONS.get(category, ""),
160
+ "tools": get_tools_for_category(category),
161
+ "loaded": category in self.active_categories,
162
+ "usage_count": self.usage_count.get(category, 0)
163
+ }
164
+ return info
165
+
166
+ def load_all_categories(self) -> int:
167
+ """Load all available categories. Returns number of categories loaded."""
168
+ count = 0
169
+ for category in get_all_categories():
170
+ if self.load_category(category):
171
+ count += 1
172
+ return count
@@ -0,0 +1,45 @@
1
+ """
2
+ BioStar X MCP Server - Tool Definitions
3
+ """
4
+
5
+ from .auth import AUTH_TOOLS
6
+ from .users import USER_TOOLS
7
+ from .doors import DOOR_TOOLS
8
+ from .access import ACCESS_TOOLS
9
+ from .devices import DEVICE_TOOLS
10
+ from .events import EVENT_TOOLS
11
+ from .audit import AUDIT_TOOLS
12
+ from .cards import CARD_TOOLS
13
+ from .navigation import NAVIGATION_TOOLS
14
+ from .occupancy import OCCUPANCY_TOOLS
15
+ from .files import FILE_TOOLS
16
+ from .logs import LOG_TOOLS
17
+ from .categories import (
18
+ TOOL_CATEGORIES,
19
+ CATEGORY_DESCRIPTIONS,
20
+ get_tools_for_category,
21
+ get_category_for_tool,
22
+ get_all_categories
23
+ )
24
+
25
+ __all__ = [
26
+ # Tool lists
27
+ "AUTH_TOOLS",
28
+ "USER_TOOLS",
29
+ "DOOR_TOOLS",
30
+ "ACCESS_TOOLS",
31
+ "DEVICE_TOOLS",
32
+ "EVENT_TOOLS",
33
+ "AUDIT_TOOLS",
34
+ "CARD_TOOLS",
35
+ "NAVIGATION_TOOLS",
36
+ "OCCUPANCY_TOOLS",
37
+ "FILE_TOOLS",
38
+ "LOG_TOOLS",
39
+ # Categories
40
+ "TOOL_CATEGORIES",
41
+ "CATEGORY_DESCRIPTIONS",
42
+ "get_tools_for_category",
43
+ "get_category_for_tool",
44
+ "get_all_categories",
45
+ ]