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,216 @@
1
+ import logging
2
+ from typing import Sequence, Dict, Any
3
+ from mcp.types import TextContent
4
+ import httpx
5
+ from .base_handler import BaseHandler
6
+ from ..utils import get_timezone_offset, get_timezone_string
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AuthHandler(BaseHandler):
12
+ """Handle authentication-related operations."""
13
+
14
+ async def login(self, args: Dict[str, Any]) -> Sequence[TextContent]:
15
+ """Login to BioStar 2."""
16
+ try:
17
+ # Get credentials from args or config
18
+ username = args.get("username") or self.session.config.biostar_username
19
+ password = args.get("password") or self.session.config.biostar_password
20
+
21
+ if not username or not password:
22
+ return self.error_response(
23
+ "Missing credentials",
24
+ {"message": "Username and password are required. Set them in .env or provide as arguments."}
25
+ )
26
+
27
+ # Make direct login API call
28
+ logger.info(f"Attempting to login to BioStar at: {self.session.config.biostar_url}")
29
+
30
+ # Create client with SSL settings
31
+ transport = None
32
+ if not self.session.config.verify_ssl:
33
+ import ssl
34
+ ssl_context = ssl.create_default_context()
35
+ ssl_context.check_hostname = False
36
+ ssl_context.verify_mode = ssl.CERT_NONE
37
+ transport = httpx.AsyncHTTPTransport(verify=ssl_context)
38
+
39
+ api_base = self.session.config.biostar_url
40
+ # api_base = "https://127.0.0.1:443"
41
+ async with httpx.AsyncClient(
42
+ transport=transport,
43
+ verify=False,
44
+ follow_redirects=True
45
+ ) as client:
46
+ logger.info(f"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Login with: {username} and {password} xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
47
+ response = await client.post(
48
+ f"{api_base}/api/login",
49
+ json={
50
+ "User": {
51
+ "login_id": username,
52
+ "password": password
53
+ }
54
+ },
55
+ headers={
56
+ "Content-Type": "application/json"
57
+ },
58
+ timeout=self.session.config.api_timeout
59
+ )
60
+
61
+ if response.status_code != 200:
62
+ return self.error_response(
63
+ f"Login failed: {response.status_code}",
64
+ {"response": response.text}
65
+ )
66
+
67
+ # Extract session ID and user info
68
+ session_id = response.headers.get("bs-session-id")
69
+ data = response.json()
70
+ user_info = data.get("User", {})
71
+
72
+ # Store session info
73
+ self.session.session_id = session_id
74
+ self.session.user_id = user_info.get("user_id")
75
+
76
+ # Update the session client to be authenticated
77
+ if not self.session.client:
78
+ await self.session.connect()
79
+ if session_id:
80
+ self.session.client.headers["bs-session-id"] = session_id
81
+
82
+ logger.info(f"Successfully logged in as user {self.session.user_id}")
83
+
84
+ return self.success_response({
85
+ "user_id": user_info.get("user_id"),
86
+ "name": user_info.get("name"),
87
+ "email": user_info.get("email"),
88
+ "session_id": session_id,
89
+ "message": "Successfully logged in to BioStar 2"
90
+ })
91
+
92
+ except httpx.ConnectTimeout as e:
93
+ return self.error_response(
94
+ "Connection timeout",
95
+ {
96
+ "message": f"Failed to connect to BioStar server at {self.session.config.biostar_url}",
97
+ "timeout": f"{self.session.config.api_timeout} seconds",
98
+ "suggestions": [
99
+ "1. Check if the BioStar server URL is correct in your .env file",
100
+ "2. Ensure the server is running and accessible",
101
+ "3. Check network connectivity to the server",
102
+ "4. Try increasing API_TIMEOUT in .env file",
103
+ "5. If using HTTPS with self-signed certificate, set VERIFY_SSL=false"
104
+ ]
105
+ }
106
+ )
107
+ except httpx.ConnectError as e:
108
+ return self.error_response(
109
+ "Connection error",
110
+ {
111
+ "message": f"Cannot connect to {self.session.config.biostar_url}",
112
+ "error": str(e),
113
+ "verify_ssl": self.session.config.verify_ssl
114
+ }
115
+ )
116
+ except Exception as e:
117
+ return await self.handle_api_error(e)
118
+
119
+ async def logout(self, args: Dict[str, Any]) -> Sequence[TextContent]:
120
+ """Logout from BioStar 2."""
121
+ try:
122
+ if not self.session.is_authenticated():
123
+ return self.error_response("Not authenticated")
124
+
125
+ # Make logout API call
126
+ async with httpx.AsyncClient(verify=self.session.config.verify_ssl) as client:
127
+ response = await client.post(
128
+ f"{self.session.config.biostar_url}/api/logout",
129
+ headers={"bs-session-id": self.session.session_id},
130
+ timeout=self.session.config.api_timeout
131
+ )
132
+
133
+ # Clear session info regardless of response
134
+ self.session.session_id = None
135
+ self.session.user_id = None
136
+ if self.session.client:
137
+ self.session.client.headers.pop("bs-session-id", None)
138
+
139
+ return self.success_response({
140
+ "message": "Successfully logged out from BioStar 2"
141
+ })
142
+
143
+ except Exception as e:
144
+ return await self.handle_api_error(e)
145
+
146
+ async def get_session_info(self, args: Dict[str, Any]) -> Sequence[TextContent]:
147
+ """Get current session information."""
148
+ try:
149
+ self.check_auth()
150
+
151
+ headers = {"bs-session-id": self.get_session_id()}
152
+
153
+ async with httpx.AsyncClient(verify=False) as client:
154
+ response = await client.get(
155
+ f"{self.session.config.biostar_url}/api/session",
156
+ headers=headers
157
+ )
158
+
159
+ if response.status_code == 200:
160
+ self.session.session_id = None
161
+ return self.success_response({
162
+ "message": "Session information retrieved successfully",
163
+ "session_data": response.json()
164
+ })
165
+ else:
166
+ return self.error_response(f"Failed to get session info: {response.status_code}")
167
+
168
+ except Exception as e:
169
+ return await self.handle_api_error(e)
170
+
171
+ async def get_server_preferences(self, args: Dict[str, Any]) -> Sequence[TextContent]:
172
+ """Get BioStar server preferences including timezone."""
173
+ try:
174
+ self.check_auth()
175
+
176
+ headers = {
177
+ "bs-session-id": self.get_session_id(),
178
+ "Content-Type": "application/json"
179
+ }
180
+
181
+ async with httpx.AsyncClient(verify=False) as client:
182
+ response = await client.get(
183
+ f"{self.session.config.biostar_url}/api/preferences/1",
184
+ headers=headers
185
+ )
186
+
187
+ if response.status_code != 200:
188
+ return self.error_response(f"Failed to get server preferences: {response.status_code} - {response.text}")
189
+
190
+ data = response.json()
191
+ preference = data.get("Preference", {})
192
+
193
+ # Get timezone info
194
+ timezone_id = preference.get("time_zone", "26") # Default to UTC+9 (Seoul)
195
+ timezone_offset_minutes = get_timezone_offset(timezone_id, default=540) # Default to +540 minutes (UTC+9)
196
+ timezone_offset_hours = timezone_offset_minutes / 60
197
+ tz_str = get_timezone_string(timezone_id, default=540)
198
+
199
+ return self.success_response({
200
+ "message": "Server preferences retrieved successfully",
201
+ "language": preference.get("language", "en"),
202
+ "timezone_id": timezone_id,
203
+ "timezone_offset_minutes": timezone_offset_minutes,
204
+ "timezone_offset_hours": timezone_offset_hours,
205
+ "timezone_string": tz_str,
206
+ "date_format": preference.get("date_format", "yyyy/MM/dd"),
207
+ "time_format": preference.get("time_format", "HH:mm"),
208
+ "user": {
209
+ "user_id": preference.get("user_id", {}).get("user_id"),
210
+ "name": preference.get("user_id", {}).get("name")
211
+ }
212
+ })
213
+
214
+ except Exception as e:
215
+ logger.exception("get_server_preferences failed")
216
+ return await self.handle_api_error(e)
@@ -0,0 +1,228 @@
1
+ """
2
+ Base Handler for BioStar X MCP Server
3
+ Provides common functionality for all API handlers
4
+ """
5
+ import logging
6
+ import json
7
+ from typing import Sequence, Dict, Any, Optional
8
+ from mcp.types import TextContent
9
+ from pydantic import ValidationError
10
+ import httpx
11
+ from ..session import BioStarSession
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BaseHandler:
17
+ """Base handler class with common functionality for all handlers."""
18
+
19
+ def __init__(self, session: BioStarSession):
20
+ self.session = session
21
+ self.client_session_id = None
22
+
23
+ def get_session_id(self) -> str:
24
+ """Get the session ID to use for API calls."""
25
+ if self.client_session_id:
26
+ logger.info(f"Using client session ID: {self.client_session_id}")
27
+ return self.client_session_id
28
+ elif self.session.session_id:
29
+ logger.info(f"Using internal session ID: {self.session.session_id}")
30
+ return self.session.session_id
31
+ else:
32
+ raise RuntimeError("No session ID available")
33
+
34
+ def check_auth(self) -> None:
35
+ """Ensure session is authenticated."""
36
+ if self.client_session_id:
37
+ logger.info("Using client session ID, skipping internal auth check")
38
+ return
39
+
40
+ if not self.session.is_authenticated():
41
+ raise RuntimeError("Not authenticated. Please login first.")
42
+
43
+ @property
44
+ def api_headers(self) -> Dict[str, str]:
45
+ """Get standard API headers with session ID."""
46
+ return {
47
+ "bs-session-id": self.get_session_id(),
48
+ "Content-Type": "application/json"
49
+ }
50
+
51
+ async def api_get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
52
+ """Make GET request to BioStar API."""
53
+ async with httpx.AsyncClient(verify=False) as client:
54
+ response = await client.get(
55
+ f"{self.session.config.biostar_url}{endpoint}",
56
+ headers=self.api_headers,
57
+ params=params,
58
+ **kwargs
59
+ )
60
+ return response
61
+
62
+ async def api_post(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
63
+ """Make POST request to BioStar API."""
64
+ async with httpx.AsyncClient(verify=False) as client:
65
+ response = await client.post(
66
+ f"{self.session.config.biostar_url}{endpoint}",
67
+ headers=self.api_headers,
68
+ json=json,
69
+ **kwargs
70
+ )
71
+ return response
72
+
73
+ async def api_put(self, endpoint: str, json: Optional[Dict[str, Any]] = None, **kwargs) -> httpx.Response:
74
+ """Make PUT request to BioStar API."""
75
+ async with httpx.AsyncClient(verify=False) as client:
76
+ response = await client.put(
77
+ f"{self.session.config.biostar_url}{endpoint}",
78
+ headers=self.api_headers,
79
+ json=json,
80
+ **kwargs
81
+ )
82
+ return response
83
+
84
+ async def api_delete(self, endpoint: str, **kwargs) -> httpx.Response:
85
+ """Make DELETE request to BioStar API."""
86
+ async with httpx.AsyncClient(verify=False) as client:
87
+ response = await client.delete(
88
+ f"{self.session.config.biostar_url}{endpoint}",
89
+ headers=self.api_headers,
90
+ **kwargs
91
+ )
92
+ return response
93
+
94
+ def success_response(self, data: Any, message: Optional[str] = None) -> list[TextContent]:
95
+ """Create a success response."""
96
+ content = {
97
+ "status": "success",
98
+ "data": data
99
+ }
100
+ if message:
101
+ content["message"] = message
102
+
103
+ return [TextContent(
104
+ type="text",
105
+ text=json.dumps(content, ensure_ascii=False, indent=2)
106
+ )]
107
+
108
+ def error_response(self, error: str, details: Optional[Dict[str, Any]] = None) -> list[TextContent]:
109
+ """Create an error response."""
110
+ content = {
111
+ "status": "error",
112
+ "error": error
113
+ }
114
+ if details:
115
+ content["details"] = details
116
+
117
+ return [TextContent(
118
+ type="text",
119
+ text=json.dumps(content, ensure_ascii=False, indent=2)
120
+ )]
121
+
122
+ async def handle_api_error(self, error: Exception) -> list[TextContent]:
123
+ """Handle API errors consistently."""
124
+ logger.error(f"API error: {error}", exc_info=True)
125
+
126
+ error_type = type(error).__name__
127
+
128
+ if hasattr(error, 'response'):
129
+ return self.error_response(
130
+ f"API error: {error.response.status_code}",
131
+ {"message": str(error), "response": error.response.text}
132
+ )
133
+ elif "TimeoutError" in str(error) or "ConnectTimeout" in str(error):
134
+ return self.error_response(
135
+ "Connection timeout",
136
+ {
137
+ "message": "Failed to connect to BioStar server. Please check:",
138
+ "suggestions": [
139
+ "1. BioStar server URL in .env file (BIOSTAR_URL)",
140
+ "2. Server is running and accessible",
141
+ "3. Network connectivity",
142
+ "4. Firewall settings"
143
+ ],
144
+ "error_type": error_type,
145
+ "details": str(error)
146
+ }
147
+ )
148
+ elif "ConnectionRefused" in str(error):
149
+ return self.error_response(
150
+ "Connection refused",
151
+ {
152
+ "message": "BioStar server refused the connection",
153
+ "suggestions": [
154
+ "1. Check if BioStar server is running",
155
+ "2. Verify the server URL and port",
156
+ "3. Check server logs"
157
+ ],
158
+ "error_type": error_type,
159
+ "details": str(error)
160
+ }
161
+ )
162
+ else:
163
+ return self.error_response(
164
+ f"Error: {error_type}",
165
+ {"message": str(error), "type": error_type}
166
+ )
167
+
168
+ def handle_validation_error(self, error: ValidationError) -> list[TextContent]:
169
+ """Handle Pydantic validation errors consistently."""
170
+ errors = []
171
+ for err in error.errors():
172
+ field = ".".join(str(loc) for loc in err["loc"])
173
+ errors.append({
174
+ "field": field,
175
+ "message": err["msg"],
176
+ "type": err["type"]
177
+ })
178
+
179
+ return self.error_response(
180
+ "Validation error",
181
+ {"errors": errors}
182
+ )
183
+
184
+ def validate_required_params(self, params: Dict[str, Any], required: list[str]) -> Optional[list[TextContent]]:
185
+ """Validate required parameters are present."""
186
+ missing = [param for param in required if param not in params]
187
+ if missing:
188
+ return self.error_response(
189
+ "Missing required parameters",
190
+ {"missing": missing}
191
+ )
192
+ return None
193
+
194
+ def format_user_info(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
195
+ """Format user information for display."""
196
+ return {
197
+ "user_id": user_data.get("user_id"),
198
+ "name": user_data.get("name"),
199
+ "email": user_data.get("email"),
200
+ "phone_number": user_data.get("phone_number"),
201
+ "user_group_id": user_data.get("user_group_id", {}).get("id"),
202
+ "disabled": user_data.get("disabled", False),
203
+ "start_datetime": user_data.get("start_datetime"),
204
+ "expiry_datetime": user_data.get("expiry_datetime")
205
+ }
206
+
207
+ def format_door_info(self, door_data: Dict[str, Any]) -> Dict[str, Any]:
208
+ """Format door information for display."""
209
+ return {
210
+ "id": door_data.get("id"),
211
+ "name": door_data.get("name"),
212
+ "description": door_data.get("description"),
213
+ "open_duration": door_data.get("open_duration"),
214
+ "open_status": door_data.get("open_status"),
215
+ "lock_status": door_data.get("lock_status"),
216
+ "unlocked": door_data.get("unlocked")
217
+ }
218
+
219
+ def format_device_info(self, device_data: Dict[str, Any]) -> Dict[str, Any]:
220
+ """Format device information for display."""
221
+ return {
222
+ "id": device_data.get("id"),
223
+ "name": device_data.get("name"),
224
+ "device_type": device_data.get("device_type", {}).get("name"),
225
+ "ip": device_data.get("ip"),
226
+ "status": device_data.get("status", {}).get("online"),
227
+ "enabled": device_data.get("enabled")
228
+ }