jarvis-ai-assistant 0.1.150__py3-none-any.whl → 0.1.152__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.
Potentially problematic release.
This version of jarvis-ai-assistant might be problematic. Click here for more details.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +6 -2
- jarvis/jarvis_git_utils/git_commiter.py +7 -0
- jarvis/jarvis_mcp/__init__.py +29 -0
- jarvis/jarvis_mcp/sse_mcp_client.py +590 -0
- jarvis/jarvis_mcp/{local_mcp_client.py → stdio_mcp_client.py} +86 -1
- jarvis/jarvis_tools/ask_codebase.py +3 -10
- jarvis/jarvis_tools/registry.py +252 -147
- jarvis/jarvis_utils/config.py +1 -1
- jarvis/jarvis_utils/methodology.py +3 -3
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info}/METADATA +32 -3
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info}/RECORD +16 -16
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info}/WHEEL +1 -1
- jarvis/jarvis_mcp/remote_mcp_client.py +0 -230
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info/licenses}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.150.dist-info → jarvis_ai_assistant-0.1.152.dist-info}/top_level.txt +0 -0
jarvis/__init__.py
CHANGED
jarvis/jarvis_agent/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Any, Callable, List, Optional, Tuple, Union
|
|
|
5
5
|
from yaspin import yaspin
|
|
6
6
|
|
|
7
7
|
from jarvis.jarvis_agent.output_handler import OutputHandler
|
|
8
|
+
from jarvis.jarvis_agent.patch import PatchOutputHandler
|
|
8
9
|
from jarvis.jarvis_platform.base import BasePlatform
|
|
9
10
|
from jarvis.jarvis_platform.registry import PlatformRegistry
|
|
10
11
|
from jarvis.jarvis_utils.output import PrettyOutput, OutputType
|
|
@@ -193,7 +194,7 @@ class Agent:
|
|
|
193
194
|
self.model.set_suppress_output(False)
|
|
194
195
|
|
|
195
196
|
from jarvis.jarvis_tools.registry import ToolRegistry
|
|
196
|
-
self.output_handler = output_handler if output_handler else [ToolRegistry()]
|
|
197
|
+
self.output_handler = output_handler if output_handler else [ToolRegistry(), PatchOutputHandler()]
|
|
197
198
|
self.multiline_inputer = multiline_inputer if multiline_inputer else get_multiline_input
|
|
198
199
|
|
|
199
200
|
self.prompt = ""
|
|
@@ -591,7 +592,10 @@ arguments:
|
|
|
591
592
|
self.prompt = f"{user_input}"
|
|
592
593
|
|
|
593
594
|
if self.first:
|
|
594
|
-
|
|
595
|
+
msg = user_input
|
|
596
|
+
for handler in self.input_handler:
|
|
597
|
+
msg, _ = handler(msg, self)
|
|
598
|
+
self.prompt = f"{user_input}\n\n以下是历史类似问题的执行经验,可参考:\n{load_methodology(msg)}"
|
|
595
599
|
self.first = False
|
|
596
600
|
|
|
597
601
|
while True:
|
|
@@ -177,6 +177,13 @@ class GitCommitTool:
|
|
|
177
177
|
# 如果成功提取,就跳出循环
|
|
178
178
|
if commit_message:
|
|
179
179
|
break
|
|
180
|
+
prompt = f"""格式错误,请按照以下格式重新生成提交信息:
|
|
181
|
+
{ot("COMMIT_MESSAGE")}
|
|
182
|
+
<类型>(<范围>): <主题>
|
|
183
|
+
|
|
184
|
+
[可选] 详细描述变更内容和原因
|
|
185
|
+
{ct("COMMIT_MESSAGE")}
|
|
186
|
+
"""
|
|
180
187
|
spinner.write("✅ 生成提交消息")
|
|
181
188
|
|
|
182
189
|
# 执行提交
|
jarvis/jarvis_mcp/__init__.py
CHANGED
|
@@ -33,4 +33,33 @@ class McpClient(ABC):
|
|
|
33
33
|
"""
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def get_resource_list(self) -> List[Dict[str, Any]]:
|
|
38
|
+
"""获取资源列表
|
|
39
|
+
|
|
40
|
+
返回:
|
|
41
|
+
List[Dict[str, Any]]: 资源列表,每个资源包含以下字段:
|
|
42
|
+
- uri: str - 资源的唯一标识符
|
|
43
|
+
- name: str - 资源的名称
|
|
44
|
+
- description: str - 资源的描述(可选)
|
|
45
|
+
- mimeType: str - 资源的MIME类型(可选)
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get_resource(self, uri: str) -> Dict[str, Any]:
|
|
51
|
+
"""获取指定资源的内容
|
|
52
|
+
|
|
53
|
+
参数:
|
|
54
|
+
uri: str - 资源的URI标识符
|
|
55
|
+
|
|
56
|
+
返回:
|
|
57
|
+
Dict[str, Any]: 资源内容,包含以下字段:
|
|
58
|
+
- uri: str - 资源的URI
|
|
59
|
+
- mimeType: str - 资源的MIME类型(可选)
|
|
60
|
+
- text: str - 文本内容(如果是文本资源)
|
|
61
|
+
- blob: str - 二进制内容(如果是二进制资源,base64编码)
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
36
65
|
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Iterator, Callable
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from urllib.parse import urljoin, urlencode, parse_qs
|
|
8
|
+
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
|
9
|
+
from . import McpClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SSEMcpClient(McpClient):
|
|
13
|
+
"""远程MCP客户端实现
|
|
14
|
+
|
|
15
|
+
参数:
|
|
16
|
+
config: 配置字典,包含以下字段:
|
|
17
|
+
- base_url: str - MCP服务器的基础URL
|
|
18
|
+
- auth_token: str - 认证令牌(可选)
|
|
19
|
+
- headers: Dict[str, str] - 额外的HTTP头(可选)
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, config: Dict[str, Any]):
|
|
22
|
+
self.config = config
|
|
23
|
+
self.base_url = config.get('base_url', '')
|
|
24
|
+
if not self.base_url:
|
|
25
|
+
raise ValueError('No base_url specified in config')
|
|
26
|
+
|
|
27
|
+
# 设置HTTP客户端
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
self.session.headers.update({
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'Accept': 'application/json',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
# 添加认证令牌(如果提供)
|
|
35
|
+
auth_token = config.get('auth_token')
|
|
36
|
+
if auth_token:
|
|
37
|
+
self.session.headers['Authorization'] = f'Bearer {auth_token}'
|
|
38
|
+
|
|
39
|
+
# 添加额外的HTTP头
|
|
40
|
+
extra_headers = config.get('headers', {})
|
|
41
|
+
self.session.headers.update(extra_headers)
|
|
42
|
+
|
|
43
|
+
# SSE相关属性
|
|
44
|
+
self.sse_response = None
|
|
45
|
+
self.sse_thread = None
|
|
46
|
+
self.messages_endpoint = None
|
|
47
|
+
self.session_id = None # 从SSE连接获取的会话ID
|
|
48
|
+
self.pending_requests = {} # 存储等待响应的请求 {id: Event}
|
|
49
|
+
self.request_results = {} # 存储请求结果 {id: result}
|
|
50
|
+
self.notification_handlers = {}
|
|
51
|
+
self.event_lock = threading.Lock()
|
|
52
|
+
self.request_id_counter = 0
|
|
53
|
+
|
|
54
|
+
# 初始化连接
|
|
55
|
+
self._initialize()
|
|
56
|
+
|
|
57
|
+
def _initialize(self) -> None:
|
|
58
|
+
"""初始化MCP连接"""
|
|
59
|
+
try:
|
|
60
|
+
# 启动SSE连接
|
|
61
|
+
self._start_sse_connection()
|
|
62
|
+
|
|
63
|
+
# 等待获取消息端点和会话ID
|
|
64
|
+
start_time = time.time()
|
|
65
|
+
while (not self.messages_endpoint or not self.session_id) and time.time() - start_time < 5:
|
|
66
|
+
time.sleep(0.1)
|
|
67
|
+
|
|
68
|
+
if not self.messages_endpoint:
|
|
69
|
+
self.messages_endpoint = "/messages" # 默认端点
|
|
70
|
+
PrettyOutput.print(f"未获取到消息端点,使用默认值: {self.messages_endpoint}", OutputType.WARNING)
|
|
71
|
+
|
|
72
|
+
if not self.session_id:
|
|
73
|
+
PrettyOutput.print("未获取到会话ID", OutputType.WARNING)
|
|
74
|
+
|
|
75
|
+
# 发送初始化请求
|
|
76
|
+
response = self._send_request('initialize', {
|
|
77
|
+
'processId': None, # 远程客户端不需要进程ID
|
|
78
|
+
'clientInfo': {
|
|
79
|
+
'name': 'jarvis',
|
|
80
|
+
'version': '1.0.0'
|
|
81
|
+
},
|
|
82
|
+
'capabilities': {},
|
|
83
|
+
'protocolVersion': "2025-03-26"
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# 验证服务器响应
|
|
87
|
+
if 'result' not in response:
|
|
88
|
+
raise RuntimeError(f"初始化失败: {response.get('error', 'Unknown error')}")
|
|
89
|
+
|
|
90
|
+
# 发送initialized通知
|
|
91
|
+
self._send_notification('notifications/initialized', {})
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
PrettyOutput.print(f"MCP初始化失败: {str(e)}", OutputType.ERROR)
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
def _start_sse_connection(self) -> None:
|
|
98
|
+
"""建立SSE连接并启动处理线程"""
|
|
99
|
+
try:
|
|
100
|
+
# 设置SSE请求头
|
|
101
|
+
sse_headers = dict(self.session.headers)
|
|
102
|
+
sse_headers.update({
|
|
103
|
+
'Accept': 'text/event-stream',
|
|
104
|
+
'Cache-Control': 'no-cache',
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# 建立SSE连接
|
|
108
|
+
sse_url = urljoin(self.base_url, 'sse')
|
|
109
|
+
self.sse_response = self.session.get(
|
|
110
|
+
sse_url,
|
|
111
|
+
stream=True,
|
|
112
|
+
headers=sse_headers,
|
|
113
|
+
timeout=30
|
|
114
|
+
)
|
|
115
|
+
self.sse_response.raise_for_status()
|
|
116
|
+
|
|
117
|
+
# 启动事件处理线程
|
|
118
|
+
self.sse_thread = threading.Thread(target=self._process_sse_events, daemon=True)
|
|
119
|
+
self.sse_thread.start()
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
PrettyOutput.print(f"SSE连接失败: {str(e)}", OutputType.ERROR)
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
def _process_sse_events(self) -> None:
|
|
126
|
+
"""处理SSE事件流"""
|
|
127
|
+
if not self.sse_response:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
buffer = ""
|
|
131
|
+
for line in self.sse_response.iter_lines(decode_unicode=True):
|
|
132
|
+
if line:
|
|
133
|
+
if line.startswith("data:"):
|
|
134
|
+
data = line[5:].strip()
|
|
135
|
+
# 检查是否包含消息端点信息
|
|
136
|
+
if data.startswith('/'):
|
|
137
|
+
# 这是消息端点信息,例如 "/messages/?session_id=xyz"
|
|
138
|
+
try:
|
|
139
|
+
# 提取消息端点路径和会话ID
|
|
140
|
+
url_parts = data.split('?')
|
|
141
|
+
self.messages_endpoint = url_parts[0]
|
|
142
|
+
|
|
143
|
+
# 如果有查询参数,尝试提取session_id
|
|
144
|
+
if len(url_parts) > 1:
|
|
145
|
+
query_string = url_parts[1]
|
|
146
|
+
query_params = parse_qs(query_string)
|
|
147
|
+
if 'session_id' in query_params:
|
|
148
|
+
self.session_id = query_params['session_id'][0]
|
|
149
|
+
except Exception as e:
|
|
150
|
+
PrettyOutput.print(f"解析消息端点或会话ID失败: {e}", OutputType.WARNING)
|
|
151
|
+
else:
|
|
152
|
+
buffer += data
|
|
153
|
+
elif line.startswith(":"): # 忽略注释行
|
|
154
|
+
continue
|
|
155
|
+
elif line.startswith("event:"): # 事件类型
|
|
156
|
+
continue # 我们不使用事件类型
|
|
157
|
+
elif line.startswith("id:"): # 事件ID
|
|
158
|
+
continue # 我们不使用事件ID
|
|
159
|
+
elif line.startswith("retry:"): # 重连时间
|
|
160
|
+
continue # 我们自己管理重连
|
|
161
|
+
else: # 空行表示事件结束
|
|
162
|
+
if buffer:
|
|
163
|
+
try:
|
|
164
|
+
self._handle_sse_event(buffer)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
PrettyOutput.print(f"处理SSE事件出错: {e}", OutputType.ERROR)
|
|
167
|
+
buffer = ""
|
|
168
|
+
|
|
169
|
+
PrettyOutput.print("SSE连接已关闭", OutputType.WARNING)
|
|
170
|
+
|
|
171
|
+
def _handle_sse_event(self, data: str) -> None:
|
|
172
|
+
"""处理单个SSE事件数据"""
|
|
173
|
+
try:
|
|
174
|
+
event_data = json.loads(data)
|
|
175
|
+
|
|
176
|
+
# 检查是请求响应还是通知
|
|
177
|
+
if 'id' in event_data:
|
|
178
|
+
# 这是一个请求的响应
|
|
179
|
+
req_id = event_data['id']
|
|
180
|
+
with self.event_lock:
|
|
181
|
+
self.request_results[req_id] = event_data
|
|
182
|
+
if req_id in self.pending_requests:
|
|
183
|
+
# 通知等待线程响应已到达
|
|
184
|
+
self.pending_requests[req_id].set()
|
|
185
|
+
elif 'method' in event_data:
|
|
186
|
+
# 这是一个通知
|
|
187
|
+
method = event_data.get('method', '')
|
|
188
|
+
params = event_data.get('params', {})
|
|
189
|
+
|
|
190
|
+
# 调用已注册的处理器
|
|
191
|
+
if method in self.notification_handlers:
|
|
192
|
+
for handler in self.notification_handlers[method]:
|
|
193
|
+
try:
|
|
194
|
+
handler(params)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
PrettyOutput.print(
|
|
197
|
+
f"处理通知时出错 ({method}): {e}",
|
|
198
|
+
OutputType.ERROR
|
|
199
|
+
)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
PrettyOutput.print(f"无法解析SSE事件: {data}", OutputType.WARNING)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
PrettyOutput.print(f"处理SSE事件时出错: {e}", OutputType.ERROR)
|
|
204
|
+
|
|
205
|
+
def register_notification_handler(self, method: str, handler: Callable) -> None:
|
|
206
|
+
"""注册通知处理器
|
|
207
|
+
|
|
208
|
+
参数:
|
|
209
|
+
method: 通知方法名
|
|
210
|
+
handler: 处理通知的回调函数,接收params参数
|
|
211
|
+
"""
|
|
212
|
+
with self.event_lock:
|
|
213
|
+
if method not in self.notification_handlers:
|
|
214
|
+
self.notification_handlers[method] = []
|
|
215
|
+
self.notification_handlers[method].append(handler)
|
|
216
|
+
|
|
217
|
+
def unregister_notification_handler(self, method: str, handler: Callable) -> None:
|
|
218
|
+
"""注销通知处理器
|
|
219
|
+
|
|
220
|
+
参数:
|
|
221
|
+
method: 通知方法名
|
|
222
|
+
handler: 要注销的处理器函数
|
|
223
|
+
"""
|
|
224
|
+
with self.event_lock:
|
|
225
|
+
if method in self.notification_handlers:
|
|
226
|
+
if handler in self.notification_handlers[method]:
|
|
227
|
+
self.notification_handlers[method].remove(handler)
|
|
228
|
+
if not self.notification_handlers[method]:
|
|
229
|
+
del self.notification_handlers[method]
|
|
230
|
+
|
|
231
|
+
def _get_next_request_id(self) -> str:
|
|
232
|
+
"""获取下一个请求ID"""
|
|
233
|
+
with self.event_lock:
|
|
234
|
+
self.request_id_counter += 1
|
|
235
|
+
return str(self.request_id_counter)
|
|
236
|
+
|
|
237
|
+
def _send_request(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
238
|
+
"""发送请求到MCP服务器
|
|
239
|
+
|
|
240
|
+
参数:
|
|
241
|
+
method: 请求方法
|
|
242
|
+
params: 请求参数
|
|
243
|
+
|
|
244
|
+
返回:
|
|
245
|
+
Dict[str, Any]: 响应结果
|
|
246
|
+
"""
|
|
247
|
+
# 生成唯一请求ID
|
|
248
|
+
req_id = self._get_next_request_id()
|
|
249
|
+
|
|
250
|
+
# 创建事件标志,用于等待响应
|
|
251
|
+
event = threading.Event()
|
|
252
|
+
|
|
253
|
+
with self.event_lock:
|
|
254
|
+
self.pending_requests[req_id] = event
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
# 构建请求
|
|
258
|
+
request = {
|
|
259
|
+
'jsonrpc': '2.0',
|
|
260
|
+
'method': method,
|
|
261
|
+
'params': params,
|
|
262
|
+
'id': req_id
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# 尝试不同的请求发送方式
|
|
266
|
+
if self.session_id:
|
|
267
|
+
# 方法1: 使用查询参数中的session_id
|
|
268
|
+
query_params = {'session_id': self.session_id}
|
|
269
|
+
messages_url = urljoin(self.base_url, self.messages_endpoint)
|
|
270
|
+
|
|
271
|
+
# 尝试直接使用原始URL(不追加查询参数)
|
|
272
|
+
try:
|
|
273
|
+
post_response = self.session.post(
|
|
274
|
+
messages_url,
|
|
275
|
+
json=request
|
|
276
|
+
)
|
|
277
|
+
post_response.raise_for_status()
|
|
278
|
+
except requests.HTTPError:
|
|
279
|
+
# 如果失败,尝试添加会话ID到查询参数
|
|
280
|
+
messages_url_with_session = f"{messages_url}?{urlencode(query_params)}"
|
|
281
|
+
post_response = self.session.post(
|
|
282
|
+
messages_url_with_session,
|
|
283
|
+
json=request
|
|
284
|
+
)
|
|
285
|
+
post_response.raise_for_status()
|
|
286
|
+
else:
|
|
287
|
+
# 方法2: 不使用session_id
|
|
288
|
+
if not self.messages_endpoint:
|
|
289
|
+
self.messages_endpoint = "/messages"
|
|
290
|
+
|
|
291
|
+
messages_url = urljoin(self.base_url, self.messages_endpoint)
|
|
292
|
+
|
|
293
|
+
# 尝试直接使用messages端点而不带任何查询参数
|
|
294
|
+
try:
|
|
295
|
+
# 尝试1: 标准JSON-RPC格式
|
|
296
|
+
post_response = self.session.post(
|
|
297
|
+
messages_url,
|
|
298
|
+
json=request
|
|
299
|
+
)
|
|
300
|
+
post_response.raise_for_status()
|
|
301
|
+
except requests.HTTPError:
|
|
302
|
+
# 尝试2: JSON字符串作为请求参数
|
|
303
|
+
post_response = self.session.post(
|
|
304
|
+
messages_url,
|
|
305
|
+
params={'request': json.dumps(request)}
|
|
306
|
+
)
|
|
307
|
+
post_response.raise_for_status()
|
|
308
|
+
|
|
309
|
+
# 等待SSE通道返回响应(最多30秒)
|
|
310
|
+
if not event.wait(timeout=30):
|
|
311
|
+
raise TimeoutError(f"等待响应超时: {method}")
|
|
312
|
+
|
|
313
|
+
# 获取响应结果
|
|
314
|
+
with self.event_lock:
|
|
315
|
+
result = self.request_results.pop(req_id, None)
|
|
316
|
+
self.pending_requests.pop(req_id, None)
|
|
317
|
+
|
|
318
|
+
if result is None:
|
|
319
|
+
raise RuntimeError(f"未收到响应: {method}")
|
|
320
|
+
|
|
321
|
+
return result
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
# 清理请求状态
|
|
325
|
+
with self.event_lock:
|
|
326
|
+
self.pending_requests.pop(req_id, None)
|
|
327
|
+
self.request_results.pop(req_id, None)
|
|
328
|
+
|
|
329
|
+
PrettyOutput.print(f"发送请求失败: {str(e)}", OutputType.ERROR)
|
|
330
|
+
raise
|
|
331
|
+
|
|
332
|
+
def _send_notification(self, method: str, params: Dict[str, Any]) -> None:
|
|
333
|
+
"""发送通知到MCP服务器(不需要响应)
|
|
334
|
+
|
|
335
|
+
参数:
|
|
336
|
+
method: 通知方法
|
|
337
|
+
params: 通知参数
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
# 构建通知
|
|
341
|
+
notification = {
|
|
342
|
+
'jsonrpc': '2.0',
|
|
343
|
+
'method': method,
|
|
344
|
+
'params': params
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# 尝试不同的请求发送方式,与_send_request保持一致
|
|
348
|
+
if self.session_id:
|
|
349
|
+
# 方法1: 使用查询参数中的session_id
|
|
350
|
+
query_params = {'session_id': self.session_id}
|
|
351
|
+
messages_url = urljoin(self.base_url, self.messages_endpoint or '/messages')
|
|
352
|
+
|
|
353
|
+
# 尝试直接使用原始URL(不追加查询参数)
|
|
354
|
+
try:
|
|
355
|
+
post_response = self.session.post(
|
|
356
|
+
messages_url,
|
|
357
|
+
json=notification
|
|
358
|
+
)
|
|
359
|
+
post_response.raise_for_status()
|
|
360
|
+
except requests.HTTPError:
|
|
361
|
+
# 如果失败,尝试添加会话ID到查询参数
|
|
362
|
+
messages_url_with_session = f"{messages_url}?{urlencode(query_params)}"
|
|
363
|
+
post_response = self.session.post(
|
|
364
|
+
messages_url_with_session,
|
|
365
|
+
json=notification
|
|
366
|
+
)
|
|
367
|
+
post_response.raise_for_status()
|
|
368
|
+
else:
|
|
369
|
+
# 方法2: 不使用session_id
|
|
370
|
+
if not self.messages_endpoint:
|
|
371
|
+
self.messages_endpoint = "/messages"
|
|
372
|
+
|
|
373
|
+
messages_url = urljoin(self.base_url, self.messages_endpoint)
|
|
374
|
+
|
|
375
|
+
# 尝试直接使用messages端点而不带任何查询参数
|
|
376
|
+
try:
|
|
377
|
+
# 尝试1: 标准JSON-RPC格式
|
|
378
|
+
post_response = self.session.post(
|
|
379
|
+
messages_url,
|
|
380
|
+
json=notification
|
|
381
|
+
)
|
|
382
|
+
post_response.raise_for_status()
|
|
383
|
+
except requests.HTTPError:
|
|
384
|
+
# 尝试2: JSON字符串作为请求参数
|
|
385
|
+
post_response = self.session.post(
|
|
386
|
+
messages_url,
|
|
387
|
+
params={'request': json.dumps(notification)}
|
|
388
|
+
)
|
|
389
|
+
post_response.raise_for_status()
|
|
390
|
+
|
|
391
|
+
except Exception as e:
|
|
392
|
+
PrettyOutput.print(f"发送通知失败: {str(e)}", OutputType.ERROR)
|
|
393
|
+
raise
|
|
394
|
+
|
|
395
|
+
def get_tool_list(self) -> List[Dict[str, Any]]:
|
|
396
|
+
"""获取工具列表
|
|
397
|
+
|
|
398
|
+
返回:
|
|
399
|
+
List[Dict[str, Any]]: 工具列表,每个工具包含以下字段:
|
|
400
|
+
- name: str - 工具名称
|
|
401
|
+
- description: str - 工具描述
|
|
402
|
+
- parameters: Dict - 工具参数
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
response = self._send_request('tools/list', {})
|
|
406
|
+
if 'result' in response and 'tools' in response['result']:
|
|
407
|
+
# 注意这里: 响应结构是 response['result']['tools']
|
|
408
|
+
tools = response['result']['tools']
|
|
409
|
+
# 将MCP协议字段转换为内部格式
|
|
410
|
+
formatted_tools = []
|
|
411
|
+
for tool in tools:
|
|
412
|
+
# 从inputSchema中提取参数定义
|
|
413
|
+
input_schema = tool.get('inputSchema', {})
|
|
414
|
+
parameters = {}
|
|
415
|
+
if 'properties' in input_schema:
|
|
416
|
+
parameters = input_schema['properties']
|
|
417
|
+
|
|
418
|
+
formatted_tools.append({
|
|
419
|
+
'name': tool.get('name', ''),
|
|
420
|
+
'description': tool.get('description', ''),
|
|
421
|
+
'parameters': parameters
|
|
422
|
+
})
|
|
423
|
+
return formatted_tools
|
|
424
|
+
else:
|
|
425
|
+
error_msg = "获取工具列表失败"
|
|
426
|
+
if 'error' in response:
|
|
427
|
+
error_msg += f": {response['error']}"
|
|
428
|
+
elif 'result' in response:
|
|
429
|
+
error_msg += f": 响应格式不正确 - {response['result']}"
|
|
430
|
+
else:
|
|
431
|
+
error_msg += ": 未知错误"
|
|
432
|
+
|
|
433
|
+
PrettyOutput.print(error_msg, OutputType.ERROR)
|
|
434
|
+
return []
|
|
435
|
+
except Exception as e:
|
|
436
|
+
PrettyOutput.print(f"获取工具列表失败: {str(e)}", OutputType.ERROR)
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
440
|
+
"""执行工具
|
|
441
|
+
|
|
442
|
+
参数:
|
|
443
|
+
tool_name: 工具名称
|
|
444
|
+
arguments: 参数字典,包含工具执行所需的参数
|
|
445
|
+
|
|
446
|
+
返回:
|
|
447
|
+
Dict[str, Any]: 执行结果,包含以下字段:
|
|
448
|
+
- success: bool - 是否执行成功
|
|
449
|
+
- stdout: str - 标准输出
|
|
450
|
+
- stderr: str - 标准错误
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
response = self._send_request('tools/call', {
|
|
454
|
+
'name': tool_name,
|
|
455
|
+
'arguments': arguments
|
|
456
|
+
})
|
|
457
|
+
if 'result' in response:
|
|
458
|
+
result = response['result']
|
|
459
|
+
# 从content中提取输出信息
|
|
460
|
+
stdout = ''
|
|
461
|
+
stderr = ''
|
|
462
|
+
for content in result.get('content', []):
|
|
463
|
+
if content.get('type') == 'text':
|
|
464
|
+
stdout += content.get('text', '')
|
|
465
|
+
elif content.get('type') == 'error':
|
|
466
|
+
stderr += content.get('text', '')
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
'success': True,
|
|
470
|
+
'stdout': stdout,
|
|
471
|
+
'stderr': stderr
|
|
472
|
+
}
|
|
473
|
+
else:
|
|
474
|
+
return {
|
|
475
|
+
'success': False,
|
|
476
|
+
'stdout': '',
|
|
477
|
+
'stderr': response.get('error', 'Unknown error')
|
|
478
|
+
}
|
|
479
|
+
except Exception as e:
|
|
480
|
+
PrettyOutput.print(f"执行工具失败: {str(e)}", OutputType.ERROR)
|
|
481
|
+
return {
|
|
482
|
+
'success': False,
|
|
483
|
+
'stdout': '',
|
|
484
|
+
'stderr': str(e)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
def get_resource_list(self) -> List[Dict[str, Any]]:
|
|
488
|
+
"""获取资源列表
|
|
489
|
+
|
|
490
|
+
返回:
|
|
491
|
+
List[Dict[str, Any]]: 资源列表,每个资源包含以下字段:
|
|
492
|
+
- uri: str - 资源的唯一标识符
|
|
493
|
+
- name: str - 资源的名称
|
|
494
|
+
- description: str - 资源的描述(可选)
|
|
495
|
+
- mimeType: str - 资源的MIME类型(可选)
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
response = self._send_request('resources/list', {})
|
|
499
|
+
if 'result' in response and 'resources' in response['result']:
|
|
500
|
+
return response['result']['resources']
|
|
501
|
+
else:
|
|
502
|
+
error_msg = "获取资源列表失败"
|
|
503
|
+
if 'error' in response:
|
|
504
|
+
error_msg += f": {response['error']}"
|
|
505
|
+
else:
|
|
506
|
+
error_msg += ": 未知错误"
|
|
507
|
+
PrettyOutput.print(error_msg, OutputType.ERROR)
|
|
508
|
+
return []
|
|
509
|
+
except Exception as e:
|
|
510
|
+
PrettyOutput.print(f"获取资源列表失败: {str(e)}", OutputType.ERROR)
|
|
511
|
+
return []
|
|
512
|
+
|
|
513
|
+
def get_resource(self, uri: str) -> Dict[str, Any]:
|
|
514
|
+
"""获取指定资源的内容
|
|
515
|
+
|
|
516
|
+
参数:
|
|
517
|
+
uri: str - 资源的URI标识符
|
|
518
|
+
|
|
519
|
+
返回:
|
|
520
|
+
Dict[str, Any]: 执行结果,包含以下字段:
|
|
521
|
+
- success: bool - 是否执行成功
|
|
522
|
+
- stdout: str - 资源内容(文本或base64编码的二进制内容)
|
|
523
|
+
- stderr: str - 错误信息
|
|
524
|
+
"""
|
|
525
|
+
try:
|
|
526
|
+
response = self._send_request('resources/read', {
|
|
527
|
+
'uri': uri
|
|
528
|
+
})
|
|
529
|
+
if 'result' in response and 'contents' in response['result']:
|
|
530
|
+
contents = response['result']['contents']
|
|
531
|
+
if contents:
|
|
532
|
+
content = contents[0] # 获取第一个资源内容
|
|
533
|
+
# 根据资源类型返回内容
|
|
534
|
+
if 'text' in content:
|
|
535
|
+
return {
|
|
536
|
+
'success': True,
|
|
537
|
+
'stdout': content['text'],
|
|
538
|
+
'stderr': ''
|
|
539
|
+
}
|
|
540
|
+
elif 'blob' in content:
|
|
541
|
+
return {
|
|
542
|
+
'success': True,
|
|
543
|
+
'stdout': content['blob'],
|
|
544
|
+
'stderr': ''
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
'success': False,
|
|
548
|
+
'stdout': '',
|
|
549
|
+
'stderr': '资源内容为空'
|
|
550
|
+
}
|
|
551
|
+
else:
|
|
552
|
+
error_msg = "获取资源内容失败"
|
|
553
|
+
if 'error' in response:
|
|
554
|
+
error_msg += f": {response['error']}"
|
|
555
|
+
else:
|
|
556
|
+
error_msg += ": 未知错误"
|
|
557
|
+
PrettyOutput.print(error_msg, OutputType.ERROR)
|
|
558
|
+
return {
|
|
559
|
+
'success': False,
|
|
560
|
+
'stdout': '',
|
|
561
|
+
'stderr': error_msg
|
|
562
|
+
}
|
|
563
|
+
except Exception as e:
|
|
564
|
+
error_msg = f"获取资源内容失败: {str(e)}"
|
|
565
|
+
PrettyOutput.print(error_msg, OutputType.ERROR)
|
|
566
|
+
return {
|
|
567
|
+
'success': False,
|
|
568
|
+
'stdout': '',
|
|
569
|
+
'stderr': error_msg
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
def __del__(self):
|
|
573
|
+
"""清理资源"""
|
|
574
|
+
# 清理请求状态
|
|
575
|
+
with self.event_lock:
|
|
576
|
+
for event in self.pending_requests.values():
|
|
577
|
+
event.set() # 释放所有等待的请求
|
|
578
|
+
self.pending_requests.clear()
|
|
579
|
+
self.request_results.clear()
|
|
580
|
+
|
|
581
|
+
# 关闭SSE响应
|
|
582
|
+
if self.sse_response:
|
|
583
|
+
try:
|
|
584
|
+
self.sse_response.close()
|
|
585
|
+
except:
|
|
586
|
+
pass
|
|
587
|
+
|
|
588
|
+
# 关闭HTTP会话
|
|
589
|
+
if self.session:
|
|
590
|
+
self.session.close()
|