astrbotmcp 0.2.2__py3-none-any.whl → 0.2.4__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/server.py +18 -0
- astrbot_mcp/tools/__init__.py +72 -0
- astrbot_mcp/tools/config_search_tool.py +196 -0
- astrbot_mcp/tools/config_tools.py +620 -0
- astrbot_mcp/tools/control_tools.py +62 -0
- astrbot_mcp/tools/helpers.py +102 -0
- astrbot_mcp/tools/log_tools.py +65 -0
- astrbot_mcp/tools/message_tools.py +501 -0
- astrbot_mcp/tools/platform_tools.py +34 -0
- astrbot_mcp/tools/plugin_market_tools.py +211 -0
- astrbot_mcp/tools/session_tools.py +596 -0
- astrbot_mcp/tools/types.py +31 -0
- astrbot_mcp/tools.py +1 -1
- {astrbotmcp-0.2.2.dist-info → astrbotmcp-0.2.4.dist-info}/METADATA +7 -3
- astrbotmcp-0.2.4.dist-info/RECORD +22 -0
- astrbotmcp-0.2.2.dist-info/RECORD +0 -11
- {astrbotmcp-0.2.2.dist-info → astrbotmcp-0.2.4.dist-info}/WHEEL +0 -0
- {astrbotmcp-0.2.2.dist-info → astrbotmcp-0.2.4.dist-info}/entry_points.txt +0 -0
- {astrbotmcp-0.2.2.dist-info → astrbotmcp-0.2.4.dist-info}/licenses/LICENSE.txt +0 -0
- {astrbotmcp-0.2.2.dist-info → astrbotmcp-0.2.4.dist-info}/top_level.txt +0 -0
astrbot_mcp/server.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
3
6
|
from fastmcp.server import FastMCP
|
|
4
7
|
|
|
5
8
|
from . import tools as astrbot_tools
|
|
@@ -62,6 +65,21 @@ def main() -> None:
|
|
|
62
65
|
|
|
63
66
|
By default this runs in stdio mode, which is what most MCP hosts expect.
|
|
64
67
|
"""
|
|
68
|
+
parser = argparse.ArgumentParser(prog="astrbot-mcp")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--version",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Print package version and exit.",
|
|
73
|
+
)
|
|
74
|
+
args = parser.parse_args()
|
|
75
|
+
|
|
76
|
+
if args.version:
|
|
77
|
+
try:
|
|
78
|
+
print(version("astrbotmcp"))
|
|
79
|
+
except PackageNotFoundError:
|
|
80
|
+
print("unknown")
|
|
81
|
+
return
|
|
82
|
+
|
|
65
83
|
server.run(transport="stdio")
|
|
66
84
|
|
|
67
85
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AstrBot MCP Tools
|
|
3
|
+
|
|
4
|
+
This module provides a collection of tools for interacting with AstrBot instances.
|
|
5
|
+
The tools are organized into separate modules by functionality:
|
|
6
|
+
|
|
7
|
+
- types: Type definitions for message parts
|
|
8
|
+
- helpers: Helper functions for file handling and error processing
|
|
9
|
+
- log_tools: Tools for retrieving AstrBot logs
|
|
10
|
+
- platform_tools: Tools for managing message platforms
|
|
11
|
+
- message_tools: Tools for sending messages to platforms
|
|
12
|
+
- session_tools: Tools for managing platform sessions
|
|
13
|
+
- control_tools: Tools for controlling AstrBot (restart, etc.)
|
|
14
|
+
|
|
15
|
+
All functions are re-exported from this module for convenience.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# 导入所有工具函数,保持向后兼容
|
|
19
|
+
from .control_tools import restart_astrbot
|
|
20
|
+
from .log_tools import get_astrbot_logs
|
|
21
|
+
from .message_tools import (
|
|
22
|
+
send_platform_message,
|
|
23
|
+
send_platform_message_direct,
|
|
24
|
+
)
|
|
25
|
+
from .platform_tools import get_message_platforms
|
|
26
|
+
from .session_tools import get_platform_session_messages
|
|
27
|
+
from .plugin_market_tools import browse_plugin_market
|
|
28
|
+
from .config_tools import (
|
|
29
|
+
list_astrbot_config_files,
|
|
30
|
+
inspect_astrbot_config,
|
|
31
|
+
apply_astrbot_config_ops,
|
|
32
|
+
)
|
|
33
|
+
from .config_search_tool import search_astrbot_config_paths
|
|
34
|
+
|
|
35
|
+
# 导入类型定义
|
|
36
|
+
from .types import MessagePart
|
|
37
|
+
|
|
38
|
+
# 导入辅助函数(内部使用)
|
|
39
|
+
from .helpers import (
|
|
40
|
+
_as_file_uri,
|
|
41
|
+
_attachment_download_url,
|
|
42
|
+
_astrbot_connect_hint,
|
|
43
|
+
_direct_media_mode,
|
|
44
|
+
_httpx_error_detail,
|
|
45
|
+
_resolve_local_file_path,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# 工具函数
|
|
50
|
+
"get_astrbot_logs",
|
|
51
|
+
"get_message_platforms",
|
|
52
|
+
"send_platform_message_direct",
|
|
53
|
+
"send_platform_message",
|
|
54
|
+
"restart_astrbot",
|
|
55
|
+
"get_platform_session_messages",
|
|
56
|
+
"browse_plugin_market",
|
|
57
|
+
"list_astrbot_config_files",
|
|
58
|
+
"inspect_astrbot_config",
|
|
59
|
+
"apply_astrbot_config_ops",
|
|
60
|
+
"search_astrbot_config_paths",
|
|
61
|
+
|
|
62
|
+
# 类型定义
|
|
63
|
+
"MessagePart",
|
|
64
|
+
|
|
65
|
+
# 辅助函数(内部使用)
|
|
66
|
+
"_resolve_local_file_path",
|
|
67
|
+
"_attachment_download_url",
|
|
68
|
+
"_astrbot_connect_hint",
|
|
69
|
+
"_httpx_error_detail",
|
|
70
|
+
"_direct_media_mode",
|
|
71
|
+
"_as_file_uri",
|
|
72
|
+
]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Union
|
|
4
|
+
|
|
5
|
+
from ..astrbot_client import AstrBotClient
|
|
6
|
+
from .helpers import _astrbot_connect_hint, _httpx_error_detail
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
JsonPathSegment = Union[str, int]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _type_name(value: Any) -> str:
|
|
13
|
+
if isinstance(value, dict):
|
|
14
|
+
return "object"
|
|
15
|
+
if isinstance(value, list):
|
|
16
|
+
return "array"
|
|
17
|
+
if value is None:
|
|
18
|
+
return "null"
|
|
19
|
+
if isinstance(value, bool):
|
|
20
|
+
return "boolean"
|
|
21
|
+
if isinstance(value, (int, float)):
|
|
22
|
+
return "number"
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
return "string"
|
|
25
|
+
return type(value).__name__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_pointer(path: List[JsonPathSegment]) -> str:
|
|
29
|
+
def esc(seg: str) -> str:
|
|
30
|
+
return seg.replace("~", "~0").replace("/", "~1")
|
|
31
|
+
|
|
32
|
+
parts: List[str] = []
|
|
33
|
+
for seg in path:
|
|
34
|
+
if isinstance(seg, int):
|
|
35
|
+
parts.append(str(seg))
|
|
36
|
+
else:
|
|
37
|
+
parts.append(esc(seg))
|
|
38
|
+
return "/" + "/".join(parts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _to_dot(path: List[JsonPathSegment]) -> str:
|
|
42
|
+
return ".".join(str(seg) for seg in path)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _match_text(haystack: str, needle: str, *, case_sensitive: bool) -> bool:
|
|
46
|
+
if not needle:
|
|
47
|
+
return False
|
|
48
|
+
if case_sensitive:
|
|
49
|
+
return needle in haystack
|
|
50
|
+
return needle.lower() in haystack.lower()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _value_to_text(value: Any) -> str | None:
|
|
54
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
55
|
+
return str(value)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _walk_find(
|
|
60
|
+
node: Any,
|
|
61
|
+
path: List[JsonPathSegment],
|
|
62
|
+
*,
|
|
63
|
+
key_query: str,
|
|
64
|
+
value_query: str | None,
|
|
65
|
+
case_sensitive: bool,
|
|
66
|
+
max_results: int,
|
|
67
|
+
results: List[Dict[str, Any]],
|
|
68
|
+
) -> None:
|
|
69
|
+
if len(results) >= max_results:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if isinstance(node, dict):
|
|
73
|
+
for k, v in node.items():
|
|
74
|
+
if len(results) >= max_results:
|
|
75
|
+
return
|
|
76
|
+
if not isinstance(k, str):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
key_ok = _match_text(k, key_query, case_sensitive=case_sensitive)
|
|
80
|
+
value_ok = True
|
|
81
|
+
if value_query is not None:
|
|
82
|
+
text = _value_to_text(v)
|
|
83
|
+
value_ok = text is not None and _match_text(
|
|
84
|
+
text, value_query, case_sensitive=case_sensitive
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if key_ok and value_ok:
|
|
88
|
+
full_path = path + [k]
|
|
89
|
+
results.append(
|
|
90
|
+
{
|
|
91
|
+
"path": full_path,
|
|
92
|
+
"path_pointer": _to_pointer(full_path),
|
|
93
|
+
"path_dot": _to_dot(full_path),
|
|
94
|
+
"type": _type_name(v),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
_walk_find(
|
|
99
|
+
v,
|
|
100
|
+
path + [k],
|
|
101
|
+
key_query=key_query,
|
|
102
|
+
value_query=value_query,
|
|
103
|
+
case_sensitive=case_sensitive,
|
|
104
|
+
max_results=max_results,
|
|
105
|
+
results=results,
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if isinstance(node, list):
|
|
110
|
+
for i, v in enumerate(node):
|
|
111
|
+
if len(results) >= max_results:
|
|
112
|
+
return
|
|
113
|
+
_walk_find(
|
|
114
|
+
v,
|
|
115
|
+
path + [i],
|
|
116
|
+
key_query=key_query,
|
|
117
|
+
value_query=value_query,
|
|
118
|
+
case_sensitive=case_sensitive,
|
|
119
|
+
max_results=max_results,
|
|
120
|
+
results=results,
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def search_astrbot_config_paths(
|
|
126
|
+
*,
|
|
127
|
+
conf_id: str | None = None,
|
|
128
|
+
system_config: bool = False,
|
|
129
|
+
key_query: str,
|
|
130
|
+
value_query: str | None = None,
|
|
131
|
+
case_sensitive: bool = False,
|
|
132
|
+
max_results: int = 50,
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Search AstrBot config and return only matched key paths (no big values).
|
|
136
|
+
|
|
137
|
+
Modes:
|
|
138
|
+
- key only: provide key_query
|
|
139
|
+
- key + value: provide key_query and value_query (matches leaf values of primitive types)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
- results: [{path, path_pointer, path_dot, type}, ...]
|
|
143
|
+
"""
|
|
144
|
+
client = AstrBotClient.from_env()
|
|
145
|
+
|
|
146
|
+
if system_config and conf_id:
|
|
147
|
+
return {"status": "error", "message": "Do not pass conf_id when system_config=true"}
|
|
148
|
+
if not system_config and not conf_id:
|
|
149
|
+
return {"status": "error", "message": "conf_id is required unless system_config=true"}
|
|
150
|
+
if not isinstance(key_query, str) or not key_query.strip():
|
|
151
|
+
return {"status": "error", "message": "key_query must be a non-empty string"}
|
|
152
|
+
if value_query is not None and (not isinstance(value_query, str) or not value_query.strip()):
|
|
153
|
+
return {"status": "error", "message": "value_query must be a non-empty string or null"}
|
|
154
|
+
if max_results <= 0:
|
|
155
|
+
return {"status": "error", "message": "max_results must be > 0"}
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
api_result = await client.get_abconf(conf_id=conf_id, system_config=system_config)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return {
|
|
161
|
+
"status": "error",
|
|
162
|
+
"message": _astrbot_connect_hint(client),
|
|
163
|
+
"base_url": client.base_url,
|
|
164
|
+
"detail": _httpx_error_detail(e),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
status = api_result.get("status")
|
|
168
|
+
if status != "ok":
|
|
169
|
+
return {"status": status, "message": api_result.get("message"), "raw": api_result}
|
|
170
|
+
|
|
171
|
+
config = (api_result.get("data") or {}).get("config")
|
|
172
|
+
if not isinstance(config, dict):
|
|
173
|
+
return {"status": "error", "message": "AstrBot returned invalid config payload", "raw": api_result}
|
|
174
|
+
|
|
175
|
+
results: List[Dict[str, Any]] = []
|
|
176
|
+
_walk_find(
|
|
177
|
+
config,
|
|
178
|
+
[],
|
|
179
|
+
key_query=key_query.strip(),
|
|
180
|
+
value_query=value_query.strip() if isinstance(value_query, str) else None,
|
|
181
|
+
case_sensitive=case_sensitive,
|
|
182
|
+
max_results=max_results,
|
|
183
|
+
results=results,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
"conf_id": conf_id,
|
|
188
|
+
"system_config": system_config,
|
|
189
|
+
"key_query": key_query,
|
|
190
|
+
"value_query": value_query,
|
|
191
|
+
"case_sensitive": case_sensitive,
|
|
192
|
+
"max_results": max_results,
|
|
193
|
+
"count": len(results),
|
|
194
|
+
"results": results,
|
|
195
|
+
}
|
|
196
|
+
|