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 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
+