astrbotmcp 0.3.0__py3-none-any.whl → 0.4.0__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.
- astrbot_mcp/astrbot_client.py +176 -17
- astrbot_mcp/config.py +0 -9
- astrbot_mcp/server.py +10 -2
- astrbot_mcp/tools/__init__.py +6 -5
- astrbot_mcp/tools/mcp_panel_tools.py +135 -0
- astrbot_mcp/tools/message/__init__.py +3 -0
- astrbot_mcp/tools/message/cache.py +23 -0
- astrbot_mcp/tools/message/direct.py +252 -0
- astrbot_mcp/tools/message/quote.py +71 -0
- astrbot_mcp/tools/message/utils.py +62 -0
- astrbot_mcp/tools/{message_tools.py → message/webchat.py} +289 -431
- astrbot_mcp/tools/plugin_admin_tools.py +344 -0
- astrbot_mcp/tools/plugin_market_tools.py +28 -0
- astrbot_mcp/tools.py +6 -2
- {astrbotmcp-0.3.0.dist-info → astrbotmcp-0.4.0.dist-info}/METADATA +59 -30
- astrbotmcp-0.4.0.dist-info/RECORD +29 -0
- {astrbotmcp-0.3.0.dist-info → astrbotmcp-0.4.0.dist-info}/WHEEL +1 -1
- astrbotmcp-0.3.0.dist-info/RECORD +0 -22
- {astrbotmcp-0.3.0.dist-info → astrbotmcp-0.4.0.dist-info}/entry_points.txt +0 -0
- {astrbotmcp-0.3.0.dist-info → astrbotmcp-0.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {astrbotmcp-0.3.0.dist-info → astrbotmcp-0.4.0.dist-info}/top_level.txt +0 -0
astrbot_mcp/astrbot_client.py
CHANGED
|
@@ -98,7 +98,16 @@ class AstrBotClient:
|
|
|
98
98
|
pwd = hashlib.md5(pwd.encode("utf-8")).hexdigest()
|
|
99
99
|
|
|
100
100
|
url = f"{self.base_url}/api/auth/login"
|
|
101
|
-
|
|
101
|
+
# SSE endpoints can legitimately stay quiet for a long time while work is happening.
|
|
102
|
+
# Use an infinite read timeout, while keeping connect/write/pool bounded.
|
|
103
|
+
client_kwargs = {
|
|
104
|
+
"timeout": httpx.Timeout(
|
|
105
|
+
connect=self.timeout,
|
|
106
|
+
read=None,
|
|
107
|
+
write=self.timeout,
|
|
108
|
+
pool=self.timeout,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
102
111
|
if self.settings.disable_proxy:
|
|
103
112
|
client_kwargs["trust_env"] = False # 禁用代理,忽略环境变量设置
|
|
104
113
|
|
|
@@ -175,9 +184,12 @@ class AstrBotClient:
|
|
|
175
184
|
max_events: Optional[int] = None,
|
|
176
185
|
) -> List[Dict[str, Any]]:
|
|
177
186
|
"""
|
|
178
|
-
Consume
|
|
187
|
+
Consume an SSE endpoint and return parsed event payloads.
|
|
179
188
|
|
|
180
|
-
AstrBot's SSE endpoints use `data: {...}\\n\\n` format per event.
|
|
189
|
+
AstrBot's SSE endpoints typically use `data: {...}\\n\\n` format per event.
|
|
190
|
+
This parser is tolerant:
|
|
191
|
+
- Supports multi-line `data:` frames per SSE spec (joined with `\\n`).
|
|
192
|
+
- If `data:` is not valid JSON, returns it as `{\"type\":\"raw\",\"data\":...}`.
|
|
181
193
|
|
|
182
194
|
`max_seconds` is a soft upper bound for how long we wait:
|
|
183
195
|
- 如果持续有事件流入,最多等待约 `max_seconds` 秒;
|
|
@@ -223,31 +235,63 @@ class AstrBotClient:
|
|
|
223
235
|
)
|
|
224
236
|
|
|
225
237
|
async def consume() -> None:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# Heartbeats / blank lines
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
if not line.startswith("data:"):
|
|
232
|
-
continue
|
|
238
|
+
current_event: str | None = None
|
|
239
|
+
data_lines: List[str] = []
|
|
233
240
|
|
|
234
|
-
|
|
235
|
-
|
|
241
|
+
def flush() -> None:
|
|
242
|
+
nonlocal current_event, data_lines
|
|
243
|
+
if not data_lines:
|
|
244
|
+
current_event = None
|
|
245
|
+
return
|
|
246
|
+
data_str = "\n".join(data_lines).strip()
|
|
247
|
+
data_lines = []
|
|
236
248
|
|
|
237
249
|
if not data_str:
|
|
238
|
-
|
|
250
|
+
current_event = None
|
|
251
|
+
return
|
|
239
252
|
|
|
240
253
|
try:
|
|
241
254
|
payload = json.loads(data_str)
|
|
242
255
|
except json.JSONDecodeError:
|
|
243
|
-
|
|
256
|
+
payload = None
|
|
244
257
|
|
|
245
258
|
if isinstance(payload, dict):
|
|
259
|
+
if current_event and "event" not in payload:
|
|
260
|
+
payload = {**payload, "event": current_event}
|
|
246
261
|
events.append(payload)
|
|
262
|
+
else:
|
|
263
|
+
events.append(
|
|
264
|
+
{
|
|
265
|
+
"type": "raw",
|
|
266
|
+
"event": current_event,
|
|
267
|
+
"data": data_str,
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
current_event = None
|
|
247
271
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
async for line in response.aiter_lines():
|
|
273
|
+
# Blank line terminates an SSE event.
|
|
274
|
+
if line == "":
|
|
275
|
+
flush()
|
|
276
|
+
if max_events is not None and len(events) >= max_events:
|
|
277
|
+
break
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# Comments / heartbeats
|
|
281
|
+
if line.startswith(":"):
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if line.startswith("event:"):
|
|
285
|
+
current_event = line.split("event:", 1)[1].strip() or None
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
if line.startswith("data:"):
|
|
289
|
+
data_lines.append(line.split("data:", 1)[1].lstrip())
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
flush()
|
|
251
295
|
|
|
252
296
|
if max_seconds is not None and max_seconds > 0:
|
|
253
297
|
try:
|
|
@@ -358,6 +402,38 @@ class AstrBotClient:
|
|
|
358
402
|
response = await self._request("POST", "/api/config/astrbot/update", json_body=payload)
|
|
359
403
|
return response.json()
|
|
360
404
|
|
|
405
|
+
async def get_plugin_config(
|
|
406
|
+
self,
|
|
407
|
+
*,
|
|
408
|
+
plugin_name: str,
|
|
409
|
+
) -> Dict[str, Any]:
|
|
410
|
+
"""
|
|
411
|
+
Get plugin config via /api/config/get?plugin_name=<name>.
|
|
412
|
+
"""
|
|
413
|
+
response = await self._request(
|
|
414
|
+
"GET",
|
|
415
|
+
"/api/config/get",
|
|
416
|
+
params={"plugin_name": plugin_name},
|
|
417
|
+
)
|
|
418
|
+
return response.json()
|
|
419
|
+
|
|
420
|
+
async def update_plugin_config(
|
|
421
|
+
self,
|
|
422
|
+
*,
|
|
423
|
+
plugin_name: str,
|
|
424
|
+
config: Dict[str, Any],
|
|
425
|
+
) -> Dict[str, Any]:
|
|
426
|
+
"""
|
|
427
|
+
Update plugin config via /api/config/plugin/update?plugin_name=<name>.
|
|
428
|
+
"""
|
|
429
|
+
response = await self._request(
|
|
430
|
+
"POST",
|
|
431
|
+
"/api/config/plugin/update",
|
|
432
|
+
params={"plugin_name": plugin_name},
|
|
433
|
+
json_body=config,
|
|
434
|
+
)
|
|
435
|
+
return response.json()
|
|
436
|
+
|
|
361
437
|
async def list_session_rules(
|
|
362
438
|
self,
|
|
363
439
|
*,
|
|
@@ -414,6 +490,40 @@ class AstrBotClient:
|
|
|
414
490
|
response = await self._request("GET", "/api/plugin/market_list", params=params or None)
|
|
415
491
|
return response.json()
|
|
416
492
|
|
|
493
|
+
async def install_plugin_from_url(
|
|
494
|
+
self,
|
|
495
|
+
*,
|
|
496
|
+
url: str,
|
|
497
|
+
proxy: str | None = None,
|
|
498
|
+
) -> Dict[str, Any]:
|
|
499
|
+
"""
|
|
500
|
+
Install a plugin from repository URL via /api/plugin/install.
|
|
501
|
+
"""
|
|
502
|
+
payload: Dict[str, Any] = {"url": url}
|
|
503
|
+
if proxy:
|
|
504
|
+
payload["proxy"] = proxy
|
|
505
|
+
response = await self._request("POST", "/api/plugin/install", json_body=payload)
|
|
506
|
+
return response.json()
|
|
507
|
+
|
|
508
|
+
async def install_plugin_from_file(
|
|
509
|
+
self,
|
|
510
|
+
file_path: str,
|
|
511
|
+
) -> Dict[str, Any]:
|
|
512
|
+
"""
|
|
513
|
+
Install a plugin from uploaded zip file via /api/plugin/install-upload.
|
|
514
|
+
"""
|
|
515
|
+
send_name = os.path.basename(file_path)
|
|
516
|
+
with open(file_path, "rb") as f:
|
|
517
|
+
files = {
|
|
518
|
+
"file": (send_name, f, "application/zip"),
|
|
519
|
+
}
|
|
520
|
+
response = await self._request(
|
|
521
|
+
"POST",
|
|
522
|
+
"/api/plugin/install-upload",
|
|
523
|
+
files=files,
|
|
524
|
+
)
|
|
525
|
+
return response.json()
|
|
526
|
+
|
|
417
527
|
# ---- Chat / platform session APIs --------------------------------
|
|
418
528
|
|
|
419
529
|
async def create_platform_session(
|
|
@@ -648,3 +758,52 @@ class AstrBotClient:
|
|
|
648
758
|
"""
|
|
649
759
|
response = await self._request("GET", "/api/stat/version")
|
|
650
760
|
return response.json()
|
|
761
|
+
|
|
762
|
+
# ---- MCP panel APIs ----------------------------------------------
|
|
763
|
+
|
|
764
|
+
async def get_mcp_servers(self) -> Dict[str, Any]:
|
|
765
|
+
"""
|
|
766
|
+
Get MCP server list from panel API /api/tools/mcp/servers.
|
|
767
|
+
"""
|
|
768
|
+
response = await self._request("GET", "/api/tools/mcp/servers")
|
|
769
|
+
return response.json()
|
|
770
|
+
|
|
771
|
+
async def add_mcp_server(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
772
|
+
"""
|
|
773
|
+
Add MCP server via panel API /api/tools/mcp/add.
|
|
774
|
+
"""
|
|
775
|
+
response = await self._request("POST", "/api/tools/mcp/add", json_body=payload)
|
|
776
|
+
return response.json()
|
|
777
|
+
|
|
778
|
+
async def update_mcp_server(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
779
|
+
"""
|
|
780
|
+
Update MCP server via panel API /api/tools/mcp/update.
|
|
781
|
+
"""
|
|
782
|
+
response = await self._request("POST", "/api/tools/mcp/update", json_body=payload)
|
|
783
|
+
return response.json()
|
|
784
|
+
|
|
785
|
+
async def delete_mcp_server(self, *, name: str) -> Dict[str, Any]:
|
|
786
|
+
"""
|
|
787
|
+
Delete MCP server via panel API /api/tools/mcp/delete.
|
|
788
|
+
"""
|
|
789
|
+
response = await self._request(
|
|
790
|
+
"POST",
|
|
791
|
+
"/api/tools/mcp/delete",
|
|
792
|
+
json_body={"name": name},
|
|
793
|
+
)
|
|
794
|
+
return response.json()
|
|
795
|
+
|
|
796
|
+
async def test_mcp_server_connection(
|
|
797
|
+
self,
|
|
798
|
+
*,
|
|
799
|
+
mcp_server_config: Dict[str, Any],
|
|
800
|
+
) -> Dict[str, Any]:
|
|
801
|
+
"""
|
|
802
|
+
Test MCP connection via panel API /api/tools/mcp/test.
|
|
803
|
+
"""
|
|
804
|
+
response = await self._request(
|
|
805
|
+
"POST",
|
|
806
|
+
"/api/tools/mcp/test",
|
|
807
|
+
json_body={"mcp_server_config": mcp_server_config},
|
|
808
|
+
)
|
|
809
|
+
return response.json()
|
astrbot_mcp/config.py
CHANGED
|
@@ -15,7 +15,6 @@ class AstrBotSettings:
|
|
|
15
15
|
default_provider: str | None = None
|
|
16
16
|
default_model: str | None = None
|
|
17
17
|
file_root: str | None = None
|
|
18
|
-
direct_media_mode: str | None = None
|
|
19
18
|
disable_proxy: bool = True # 默认禁用代理,防止本地请求被代理拦截
|
|
20
19
|
|
|
21
20
|
|
|
@@ -41,10 +40,6 @@ def get_settings() -> AstrBotSettings:
|
|
|
41
40
|
- ASTRBOT_DEFAULT_PROVIDER: Default provider id to use for /api/chat/send.
|
|
42
41
|
- ASTRBOT_DEFAULT_MODEL: Default model id to use for /api/chat/send.
|
|
43
42
|
- ASTRBOTMCP_FILE_ROOT: Base directory for resolving relative local file_path.
|
|
44
|
-
- ASTRBOTMCP_DIRECT_MEDIA_MODE: How send_platform_message_direct handles local media:
|
|
45
|
-
- auto (default): try local path first, then fallback to upload+URL.
|
|
46
|
-
- local: always send local absolute paths to AstrBot platform adapters.
|
|
47
|
-
- upload: upload to AstrBot first and send an http(s) URL.
|
|
48
43
|
"""
|
|
49
44
|
base_url = _get_env("ASTRBOT_BASE_URL")
|
|
50
45
|
if not base_url:
|
|
@@ -69,9 +64,6 @@ def get_settings() -> AstrBotSettings:
|
|
|
69
64
|
default_provider = _get_env("ASTRBOT_DEFAULT_PROVIDER")
|
|
70
65
|
default_model = _get_env("ASTRBOT_DEFAULT_MODEL")
|
|
71
66
|
file_root = _get_env("ASTRBOTMCP_FILE_ROOT") or _get_env("ASTRBOT_MCP_FILE_ROOT")
|
|
72
|
-
direct_media_mode = _get_env("ASTRBOTMCP_DIRECT_MEDIA_MODE") or _get_env(
|
|
73
|
-
"ASTRBOT_MCP_DIRECT_MEDIA_MODE"
|
|
74
|
-
)
|
|
75
67
|
|
|
76
68
|
# 默认禁用代理,除非明确设置为false
|
|
77
69
|
disable_proxy_str = _get_env("ASTRBOTMCP_DISABLE_PROXY")
|
|
@@ -87,6 +79,5 @@ def get_settings() -> AstrBotSettings:
|
|
|
87
79
|
default_provider=default_provider,
|
|
88
80
|
default_model=default_model,
|
|
89
81
|
file_root=file_root,
|
|
90
|
-
direct_media_mode=direct_media_mode,
|
|
91
82
|
disable_proxy=disable_proxy,
|
|
92
83
|
)
|
astrbot_mcp/server.py
CHANGED
|
@@ -15,6 +15,7 @@ server = FastMCP(
|
|
|
15
15
|
"MCP server for interacting with an existing AstrBot instance. "
|
|
16
16
|
"Provides tools to read logs, list configured message platforms, "
|
|
17
17
|
"send message chains (including files) via the web chat API, "
|
|
18
|
+
"install/configure plugins, manage MCP panel config, "
|
|
18
19
|
"restart AstrBot core, read platform session message history, "
|
|
19
20
|
"and browse the AstrBot plugin market."
|
|
20
21
|
),
|
|
@@ -23,7 +24,6 @@ server = FastMCP(
|
|
|
23
24
|
# Register tools with FastMCP
|
|
24
25
|
server.tool(astrbot_tools.get_astrbot_logs, name="get_astrbot_logs")
|
|
25
26
|
server.tool(astrbot_tools.get_message_platforms, name="get_message_platforms")
|
|
26
|
-
server.tool(astrbot_tools.send_platform_message_direct, name="send_platform_message_direct")
|
|
27
27
|
server.tool(astrbot_tools.send_platform_message, name="send_platform_message")
|
|
28
28
|
server.tool(astrbot_tools.restart_astrbot, name="restart_astrbot")
|
|
29
29
|
server.tool(
|
|
@@ -31,6 +31,12 @@ server.tool(
|
|
|
31
31
|
name="get_platform_session_messages",
|
|
32
32
|
)
|
|
33
33
|
server.tool(astrbot_tools.browse_plugin_market, name="browse_plugin_market")
|
|
34
|
+
server.tool(astrbot_tools.install_astrbot_plugin, name="install_astrbot_plugin")
|
|
35
|
+
server.tool(
|
|
36
|
+
astrbot_tools.configure_astrbot_plugin_json,
|
|
37
|
+
name="configure_astrbot_plugin_json",
|
|
38
|
+
)
|
|
39
|
+
server.tool(astrbot_tools.manage_mcp_config_panel, name="manage_mcp_config_panel")
|
|
34
40
|
server.tool(astrbot_tools.list_astrbot_config_files, name="list_astrbot_config_files")
|
|
35
41
|
server.tool(astrbot_tools.inspect_astrbot_config, name="inspect_astrbot_config")
|
|
36
42
|
server.tool(astrbot_tools.apply_astrbot_config_ops, name="apply_astrbot_config_ops")
|
|
@@ -49,10 +55,12 @@ def astrbot_info():
|
|
|
49
55
|
"get_astrbot_logs",
|
|
50
56
|
"get_message_platforms",
|
|
51
57
|
"send_platform_message",
|
|
52
|
-
"send_platform_message_direct",
|
|
53
58
|
"restart_astrbot",
|
|
54
59
|
"get_platform_session_messages",
|
|
55
60
|
"browse_plugin_market",
|
|
61
|
+
"install_astrbot_plugin",
|
|
62
|
+
"configure_astrbot_plugin_json",
|
|
63
|
+
"manage_mcp_config_panel",
|
|
56
64
|
"list_astrbot_config_files",
|
|
57
65
|
"inspect_astrbot_config",
|
|
58
66
|
"apply_astrbot_config_ops",
|
astrbot_mcp/tools/__init__.py
CHANGED
|
@@ -18,13 +18,14 @@ All functions are re-exported from this module for convenience.
|
|
|
18
18
|
# 导入所有工具函数,保持向后兼容
|
|
19
19
|
from .control_tools import restart_astrbot
|
|
20
20
|
from .log_tools import get_astrbot_logs
|
|
21
|
-
from .
|
|
21
|
+
from .message import (
|
|
22
22
|
send_platform_message,
|
|
23
|
-
send_platform_message_direct,
|
|
24
23
|
)
|
|
25
24
|
from .platform_tools import get_message_platforms
|
|
26
25
|
from .session_tools import get_platform_session_messages
|
|
27
26
|
from .plugin_market_tools import browse_plugin_market
|
|
27
|
+
from .plugin_admin_tools import install_astrbot_plugin, configure_astrbot_plugin_json
|
|
28
|
+
from .mcp_panel_tools import manage_mcp_config_panel
|
|
28
29
|
from .config_tools import (
|
|
29
30
|
list_astrbot_config_files,
|
|
30
31
|
inspect_astrbot_config,
|
|
@@ -40,7 +41,6 @@ from .helpers import (
|
|
|
40
41
|
_as_file_uri,
|
|
41
42
|
_attachment_download_url,
|
|
42
43
|
_astrbot_connect_hint,
|
|
43
|
-
_direct_media_mode,
|
|
44
44
|
_httpx_error_detail,
|
|
45
45
|
_resolve_local_file_path,
|
|
46
46
|
)
|
|
@@ -49,11 +49,13 @@ __all__ = [
|
|
|
49
49
|
# 工具函数
|
|
50
50
|
"get_astrbot_logs",
|
|
51
51
|
"get_message_platforms",
|
|
52
|
-
"send_platform_message_direct",
|
|
53
52
|
"send_platform_message",
|
|
54
53
|
"restart_astrbot",
|
|
55
54
|
"get_platform_session_messages",
|
|
56
55
|
"browse_plugin_market",
|
|
56
|
+
"install_astrbot_plugin",
|
|
57
|
+
"configure_astrbot_plugin_json",
|
|
58
|
+
"manage_mcp_config_panel",
|
|
57
59
|
"list_astrbot_config_files",
|
|
58
60
|
"inspect_astrbot_config",
|
|
59
61
|
"apply_astrbot_config_ops",
|
|
@@ -67,6 +69,5 @@ __all__ = [
|
|
|
67
69
|
"_attachment_download_url",
|
|
68
70
|
"_astrbot_connect_hint",
|
|
69
71
|
"_httpx_error_detail",
|
|
70
|
-
"_direct_media_mode",
|
|
71
72
|
"_as_file_uri",
|
|
72
73
|
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Literal, Optional
|
|
4
|
+
|
|
5
|
+
from ..astrbot_client import AstrBotClient
|
|
6
|
+
from .helpers import _astrbot_connect_hint, _httpx_error_detail
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _get_astrbot_log_tail(
|
|
10
|
+
client: AstrBotClient,
|
|
11
|
+
*,
|
|
12
|
+
limit: int = 120,
|
|
13
|
+
) -> Dict[str, Any] | None:
|
|
14
|
+
try:
|
|
15
|
+
hist = await client.get_log_history()
|
|
16
|
+
except Exception as e:
|
|
17
|
+
return {
|
|
18
|
+
"status": "error",
|
|
19
|
+
"message": f"AstrBot API error: {getattr(getattr(e, 'response', None), 'status_code', None) or 'Unknown'}",
|
|
20
|
+
"detail": _httpx_error_detail(e),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if hist.get("status") != "ok":
|
|
24
|
+
return {
|
|
25
|
+
"status": hist.get("status"),
|
|
26
|
+
"message": hist.get("message"),
|
|
27
|
+
"raw": hist,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logs = (hist.get("data") or {}).get("logs", [])
|
|
31
|
+
if not isinstance(logs, list):
|
|
32
|
+
return {
|
|
33
|
+
"status": "error",
|
|
34
|
+
"message": "Unexpected /api/log-history response shape (logs is not a list).",
|
|
35
|
+
"raw": hist,
|
|
36
|
+
}
|
|
37
|
+
return {"status": "ok", "logs": logs[-max(1, int(limit)) :]}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def manage_mcp_config_panel(
|
|
41
|
+
action: Literal["list", "add", "update", "delete", "test"] = "list",
|
|
42
|
+
name: Optional[str] = None,
|
|
43
|
+
server_config: Optional[Dict[str, Any]] = None,
|
|
44
|
+
active: Optional[bool] = None,
|
|
45
|
+
include_logs: bool = True,
|
|
46
|
+
log_tail_limit: int = 120,
|
|
47
|
+
) -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Manage AstrBot MCP config panel APIs.
|
|
50
|
+
|
|
51
|
+
Actions:
|
|
52
|
+
- list: GET /api/tools/mcp/servers
|
|
53
|
+
- add: POST /api/tools/mcp/add
|
|
54
|
+
- update: POST /api/tools/mcp/update
|
|
55
|
+
- delete: POST /api/tools/mcp/delete
|
|
56
|
+
- test: POST /api/tools/mcp/test
|
|
57
|
+
"""
|
|
58
|
+
client = AstrBotClient.from_env()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
if action == "list":
|
|
62
|
+
raw = await client.get_mcp_servers()
|
|
63
|
+
elif action == "add":
|
|
64
|
+
if not name or not str(name).strip():
|
|
65
|
+
return {"status": "error", "message": "name is required for action='add'."}
|
|
66
|
+
if not isinstance(server_config, dict) or not server_config:
|
|
67
|
+
return {
|
|
68
|
+
"status": "error",
|
|
69
|
+
"message": "server_config is required for action='add'.",
|
|
70
|
+
}
|
|
71
|
+
payload = {"name": str(name).strip(), **server_config}
|
|
72
|
+
if active is not None:
|
|
73
|
+
payload["active"] = bool(active)
|
|
74
|
+
raw = await client.add_mcp_server(payload)
|
|
75
|
+
elif action == "update":
|
|
76
|
+
if not name or not str(name).strip():
|
|
77
|
+
return {"status": "error", "message": "name is required for action='update'."}
|
|
78
|
+
payload = {"name": str(name).strip()}
|
|
79
|
+
if isinstance(server_config, dict):
|
|
80
|
+
payload.update(server_config)
|
|
81
|
+
if active is not None:
|
|
82
|
+
payload["active"] = bool(active)
|
|
83
|
+
raw = await client.update_mcp_server(payload)
|
|
84
|
+
elif action == "delete":
|
|
85
|
+
if not name or not str(name).strip():
|
|
86
|
+
return {
|
|
87
|
+
"status": "error",
|
|
88
|
+
"message": "name is required for action='delete'.",
|
|
89
|
+
}
|
|
90
|
+
raw = await client.delete_mcp_server(name=str(name).strip())
|
|
91
|
+
else: # test
|
|
92
|
+
if not isinstance(server_config, dict) or not server_config:
|
|
93
|
+
return {
|
|
94
|
+
"status": "error",
|
|
95
|
+
"message": "server_config is required for action='test'.",
|
|
96
|
+
}
|
|
97
|
+
raw = await client.test_mcp_server_connection(
|
|
98
|
+
mcp_server_config=server_config
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
payload = {
|
|
102
|
+
"status": "error",
|
|
103
|
+
"message": _astrbot_connect_hint(client),
|
|
104
|
+
"base_url": client.base_url,
|
|
105
|
+
"detail": _httpx_error_detail(e),
|
|
106
|
+
"action": action,
|
|
107
|
+
"name": name,
|
|
108
|
+
}
|
|
109
|
+
if include_logs:
|
|
110
|
+
payload["astrbot_logs_tail"] = await _get_astrbot_log_tail(
|
|
111
|
+
client, limit=log_tail_limit
|
|
112
|
+
)
|
|
113
|
+
return payload
|
|
114
|
+
|
|
115
|
+
payload: Dict[str, Any] = {
|
|
116
|
+
"status": raw.get("status", "ok"),
|
|
117
|
+
"message": raw.get("message"),
|
|
118
|
+
"action": action,
|
|
119
|
+
"name": name,
|
|
120
|
+
"raw": raw,
|
|
121
|
+
}
|
|
122
|
+
if action == "list" and raw.get("status") == "ok":
|
|
123
|
+
servers = raw.get("data") if isinstance(raw.get("data"), list) else []
|
|
124
|
+
payload["servers"] = servers
|
|
125
|
+
payload["mcp_server_errlogs"] = [
|
|
126
|
+
{"name": s.get("name"), "errlogs": s.get("errlogs")}
|
|
127
|
+
for s in servers
|
|
128
|
+
if isinstance(s, dict) and s.get("errlogs")
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
if include_logs:
|
|
132
|
+
payload["astrbot_logs_tail"] = await _get_astrbot_log_tail(
|
|
133
|
+
client, limit=log_tail_limit
|
|
134
|
+
)
|
|
135
|
+
return payload
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Dict, Tuple
|
|
5
|
+
|
|
6
|
+
from ...astrbot_client import AstrBotClient
|
|
7
|
+
|
|
8
|
+
_SESSION_CACHE_LOCK = asyncio.Lock()
|
|
9
|
+
_SESSION_CACHE: Dict[Tuple[str, str, str], str] = {}
|
|
10
|
+
|
|
11
|
+
_LAST_SAVED_MESSAGE_ID_LOCK = asyncio.Lock()
|
|
12
|
+
_LAST_SAVED_MESSAGE_ID_BY_SESSION: Dict[Tuple[str, str, str], str] = {}
|
|
13
|
+
|
|
14
|
+
_LAST_USER_MESSAGE_ID_LOCK = asyncio.Lock()
|
|
15
|
+
_LAST_USER_MESSAGE_ID_BY_SESSION: Dict[Tuple[str, str, str], str] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _session_cache_key(client: AstrBotClient, platform_id: str) -> Tuple[str, str, str]:
|
|
19
|
+
return (client.base_url, client.settings.username or "", platform_id)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _last_saved_key(client: AstrBotClient, session_id: str) -> Tuple[str, str, str]:
|
|
23
|
+
return (client.base_url, client.settings.username or "", session_id)
|