py-mcpdock-cli 1.0.13__py3-none-any.whl → 1.0.18__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.
- cli/commands/install.py +78 -81
- cli/commands/query_pid.py +36 -0
- cli/commands/run.py +9 -11
- cli/main.py +6 -2
- cli/runners/stdio_runner.py +112 -320
- cli/utils/config.py +120 -21
- cli/utils/logger.py +4 -4
- cli/utils/mcp_utils.py +314 -0
- cli/utils/process_utils.py +656 -0
- cli/utils/run_query_pid_by_packagename.py +62 -0
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.18.dist-info}/METADATA +3 -1
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.18.dist-info}/RECORD +15 -11
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.18.dist-info}/WHEEL +1 -1
- py_mcpdock_cli-1.0.18.dist-info/entry_points.txt +2 -0
- py_mcpdock_cli-1.0.13.dist-info/entry_points.txt +0 -2
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.18.dist-info}/top_level.txt +0 -0
cli/runners/stdio_runner.py
CHANGED
@@ -7,327 +7,28 @@ through standard input/output pipes.
|
|
7
7
|
import json
|
8
8
|
import asyncio
|
9
9
|
from logging import error
|
10
|
+
import mcp.types as types
|
10
11
|
import signal
|
11
12
|
import sys
|
12
13
|
import os
|
13
14
|
import anyio
|
15
|
+
import threading
|
14
16
|
from typing import Dict, Any, Optional, Awaitable
|
15
|
-
from rich import print as rprint
|
16
17
|
from contextlib import AsyncExitStack
|
17
|
-
|
18
|
+
|
18
19
|
from ..utils.logger import verbose
|
19
20
|
from ..types.registry import RegistryServer
|
20
21
|
from ..utils.runtime import get_runtime_environment
|
21
|
-
from mcp import StdioServerParameters
|
22
|
+
from mcp import StdioServerParameters, ClientSession
|
22
23
|
from mcp.client.stdio import stdio_client
|
23
|
-
from mcp.types import ClientRequest, ServerRequest, JSONRPCRequest, JSONRPCMessage
|
24
|
-
|
25
|
-
|
26
|
-
# 定义一个通用的 Model,用于接收任何 JSON 响应
|
27
|
-
class DictModel(BaseModel):
|
28
|
-
@classmethod
|
29
|
-
def model_validate(cls, value):
|
30
|
-
if isinstance(value, dict):
|
31
|
-
return value
|
32
|
-
return dict(value)
|
33
|
-
|
34
|
-
|
35
|
-
# 处理客户端请求
|
36
|
-
async def process_client_request(message, session):
|
37
|
-
"""处理从客户端接收的请求并转发到服务器"""
|
38
|
-
original_id = message.get("id")
|
39
|
-
method = message.get("method")
|
40
|
-
params = message.get("params", {})
|
41
|
-
|
42
|
-
# 添加特殊处理,记录初始化请求的详细信息
|
43
|
-
if method == "initialize":
|
44
|
-
verbose(f"[Runner] 收到初始化请求 ID: {original_id}, 详细内容: {json.dumps(message)}")
|
45
|
-
|
46
|
-
# 对初始化请求使用SDK的initialize方法,而不是简单转发
|
47
|
-
try:
|
48
|
-
verbose("[Runner] 使用SDK的initialize方法处理初始化请求")
|
49
|
-
# 直接调用session.initialize()获取规范的响应
|
50
|
-
init_result = await session.initialize()
|
51
|
-
verbose(f"[Runner] SDK初始化原始响应类型: {type(init_result)}")
|
52
|
-
verbose(f"[Runner] SDK初始化原始响应属性: {dir(init_result)}")
|
53
|
-
|
54
|
-
# 检查响应内容的具体格式
|
55
|
-
if hasattr(init_result, "model_dump"):
|
56
|
-
result_content = init_result.model_dump()
|
57
|
-
verbose(f"[Runner] 使用model_dump()解析结果: {json.dumps(result_content)[:200]}...")
|
58
|
-
elif hasattr(init_result, "dict"):
|
59
|
-
result_content = init_result.dict()
|
60
|
-
verbose(f"[Runner] 使用dict()解析结果: {json.dumps(result_content)[:200]}...")
|
61
|
-
else:
|
62
|
-
result_content = init_result
|
63
|
-
verbose(
|
64
|
-
f"[Runner] 直接使用结果: {json.dumps(result_content)[:200] if isinstance(result_content, dict) else str(result_content)[:200]}...")
|
65
|
-
|
66
|
-
# 构造完整的JSON-RPC响应
|
67
|
-
response = json.dumps({
|
68
|
-
"jsonrpc": "2.0",
|
69
|
-
"id": original_id,
|
70
|
-
"result": result_content
|
71
|
-
})
|
72
|
-
|
73
|
-
verbose(f"[Runner] 通过SDK获取到规范的初始化响应: {response[:200]}...")
|
74
|
-
|
75
|
-
# 再额外发送一个initialized通知给上游客户端吗?
|
76
|
-
# 通常客户端接收到initialize响应后,会自己发送initialized通知
|
77
|
-
# 所以这里不需要主动发送
|
78
|
-
|
79
|
-
return response
|
80
|
-
except Exception as e:
|
81
|
-
error(f"[Runner] SDK初始化失败: {str(e)}")
|
82
|
-
import traceback
|
83
|
-
error(f"[Runner] 异常堆栈: {traceback.format_exc()}")
|
84
|
-
# 如果SDK初始化失败,回退到常规请求处理
|
85
|
-
verbose("[Runner] 回退到常规请求处理方式")
|
86
|
-
|
87
|
-
verbose(f"[stdin] Processing request with id: {original_id}, method: {method}")
|
88
|
-
|
89
|
-
# 常规请求处理:确定请求类型和构建请求对象
|
90
|
-
req_obj = create_request_object(message, method)
|
91
|
-
|
92
|
-
# 发送请求并处理响应
|
93
|
-
try:
|
94
|
-
verbose(f"[Runner] 向下游服务器发送请求,method: {method}, id: {original_id}")
|
95
|
-
result = await send_request_with_timeout(session, req_obj, original_id)
|
96
|
-
|
97
|
-
if method == "initialize":
|
98
|
-
verbose(f"[Runner] 收到初始化响应: {result}")
|
99
|
-
|
100
|
-
return result
|
101
|
-
except Exception as e:
|
102
|
-
error(f"[Runner] 请求处理异常 ({method}): {str(e)}")
|
103
|
-
raise
|
104
|
-
|
105
|
-
|
106
|
-
# 创建请求对象
|
107
|
-
def create_request_object(message, method):
|
108
|
-
"""根据方法类型创建适当的请求对象"""
|
109
|
-
# 作为代理,我们不需要严格验证方法是否符合标准列表
|
110
|
-
# 直接创建ClientRequest对象,透明转发所有请求
|
111
|
-
msg = dict(message)
|
112
|
-
msg.pop("jsonrpc", None) # 移除 jsonrpc 字段,SDK会自动添加
|
113
|
-
msg.pop("id", None) # 移除 id 字段,我们会在响应中重新添加
|
114
|
-
|
115
|
-
try:
|
116
|
-
# 尝试创建 ClientRequest
|
117
|
-
return ClientRequest(**msg)
|
118
|
-
except Exception as e:
|
119
|
-
# 如果创建失败,回退到 ServerRequest
|
120
|
-
verbose(f"[Runner] 创建 ClientRequest 失败,回退到 ServerRequest: {str(e)}")
|
121
|
-
return ServerRequest(method=method, params=message.get("params", {}))
|
122
|
-
|
123
|
-
|
124
|
-
async def send_request_with_timeout(session, req_obj, original_id, timeout_seconds=60):
|
125
|
-
"""发送请求并处理超时和错误情况"""
|
126
|
-
try:
|
127
|
-
# 初始化 resp 为 None,防止超时时未定义
|
128
|
-
resp = None
|
129
|
-
# 使用超时机制
|
130
|
-
with anyio.move_on_after(timeout_seconds):
|
131
|
-
# 记录请求信息
|
132
|
-
verbose(f"[Runner] 发送请求,原始ID={original_id}, 方法={req_obj.method if hasattr(req_obj, 'method') else '未知'}")
|
133
|
-
|
134
|
-
# 发送请求并等待响应
|
135
|
-
resp = await session.send_request(req_obj, DictModel)
|
136
|
-
|
137
|
-
if resp:
|
138
|
-
# 直接使用原始ID构造响应
|
139
|
-
return json.dumps({
|
140
|
-
"id": original_id,
|
141
|
-
"jsonrpc": "2.0",
|
142
|
-
"result": resp
|
143
|
-
})
|
144
|
-
else:
|
145
|
-
return json.dumps({
|
146
|
-
"id": original_id,
|
147
|
-
"jsonrpc": "2.0",
|
148
|
-
"error": {
|
149
|
-
"code": -32000,
|
150
|
-
"message": "Request timed out or empty response"
|
151
|
-
}
|
152
|
-
})
|
153
|
-
|
154
|
-
except Exception as e:
|
155
|
-
# 处理请求错误
|
156
|
-
error_msg = str(e)
|
157
|
-
error_code = -32603
|
158
|
-
|
159
|
-
# 分类错误类型
|
160
|
-
if "timed out" in error_msg.lower():
|
161
|
-
error_code = -32001
|
162
|
-
elif "connection" in error_msg.lower():
|
163
|
-
error_code = -32002
|
164
|
-
|
165
|
-
return json.dumps({
|
166
|
-
"id": original_id,
|
167
|
-
"jsonrpc": "2.0",
|
168
|
-
"error": {
|
169
|
-
"code": error_code,
|
170
|
-
"message": f"Request failed: {error_msg}"
|
171
|
-
}
|
172
|
-
})
|
173
|
-
|
174
|
-
|
175
|
-
# 初始化 MCP 会话
|
176
|
-
async def initialize_session(session):
|
177
|
-
"""初始化 MCP 协议会话,转发上游客户端的初始化请求到下游服务器"""
|
178
|
-
try:
|
179
|
-
# 关键点: 作为代理,我们不应该主动调用 session.initialize()
|
180
|
-
# 上游客户端会发送初始化请求,我们应该在 handle_stdin 函数中处理
|
181
|
-
verbose("[Runner] MCP 代理准备就绪,等待上游客户端的初始化请求...")
|
182
|
-
return True
|
183
|
-
except Exception as init_error:
|
184
|
-
error_msg = f"代理初始化失败: {str(init_error)}"
|
185
|
-
error(f"[Runner] {error_msg}")
|
186
|
-
return False
|
187
|
-
|
188
|
-
|
189
|
-
# 处理来自标准输入的消息
|
190
|
-
async def handle_stdin(session, is_shutting_down):
|
191
|
-
"""处理从标准输入接收的消息并转发到服务器"""
|
192
|
-
loop = asyncio.get_event_loop()
|
193
|
-
|
194
|
-
while not is_shutting_down:
|
195
|
-
line = await loop.run_in_executor(None, sys.stdin.readline)
|
196
|
-
if not line:
|
197
|
-
break
|
198
|
-
|
199
|
-
try:
|
200
|
-
message = json.loads(line)
|
201
|
-
verbose(f"[stdin] Received message: {line.strip()}")
|
202
|
-
|
203
|
-
method = message.get("method", "")
|
204
|
-
# 根据消息类型处理
|
205
|
-
if "id" in message: # 这是请求,需要响应
|
206
|
-
response = await process_client_request(message, session)
|
207
|
-
sys.stdout.write(response + "\n")
|
208
|
-
sys.stdout.flush()
|
209
|
-
verbose(f"[stdin] Response sent for method: {method}")
|
210
|
-
else: # 这是通知,不需要响应
|
211
|
-
await session.send(message)
|
212
|
-
verbose(f"[stdin] Notification sent for method: {method}")
|
213
|
-
|
214
|
-
verbose(f"[stdin] Processed: {line.strip()}")
|
215
|
-
|
216
|
-
except json.JSONDecodeError as e:
|
217
|
-
error(f"[stdin] JSON decode error: {e}")
|
218
|
-
except Exception as e:
|
219
|
-
error(f"[stdin] Error processing input: {e}")
|
220
|
-
# 如果是请求(有ID),才需要发送错误响应
|
221
|
-
try:
|
222
|
-
if 'message' in locals() and isinstance(message, dict) and "id" in message:
|
223
|
-
error_resp = json.dumps({
|
224
|
-
"jsonrpc": "2.0",
|
225
|
-
"id": message.get("id"),
|
226
|
-
"error": {
|
227
|
-
"code": -32700,
|
228
|
-
"message": f"Parse error: {str(e)}"
|
229
|
-
}
|
230
|
-
})
|
231
|
-
sys.stdout.write(error_resp + "\n")
|
232
|
-
sys.stdout.flush()
|
233
|
-
verbose(f"[stdin] Sent error response for parse error")
|
234
|
-
except Exception as err:
|
235
|
-
error(f"[stdin] Failed to send error response: {err}")
|
236
|
-
|
237
|
-
|
238
|
-
# 处理单个服务器消息
|
239
|
-
async def handle_single_server_message(data):
|
240
|
-
"""处理单个从服务器接收的消息并输出到标准输出"""
|
241
|
-
try:
|
242
|
-
# 记录接收到的原始数据类型,帮助调试
|
243
|
-
verbose(f"[server_raw] Received data type: {type(data)}")
|
244
|
-
|
245
|
-
# 尝试获取原始数据的字符串表示用于调试
|
246
|
-
raw_data_str = str(data)
|
247
|
-
if len(raw_data_str) > 500:
|
248
|
-
raw_data_str = raw_data_str[:500] + "..."
|
249
|
-
verbose(f"[server_raw] 原始数据: {raw_data_str}")
|
250
|
-
|
251
|
-
# 根据数据类型进行处理
|
252
|
-
if hasattr(data, "model_dump"):
|
253
|
-
content = data.model_dump()
|
254
|
-
verbose(f"[server_raw] Processed pydantic v2 model: {type(data)}")
|
255
|
-
elif hasattr(data, "dict"):
|
256
|
-
content = data.dict()
|
257
|
-
verbose(f"[server_raw] Processed pydantic v1 model: {type(data)}")
|
258
|
-
elif isinstance(data, dict):
|
259
|
-
content = data
|
260
|
-
verbose(f"[server_raw] Processed dict with {len(data)} keys")
|
261
|
-
else:
|
262
|
-
# 尝试转换为字符串,然后解析为JSON
|
263
|
-
try:
|
264
|
-
content = json.loads(str(data))
|
265
|
-
verbose(f"[server_raw] Converted to JSON: {type(data)}")
|
266
|
-
except:
|
267
|
-
content = {"data": str(data)}
|
268
|
-
verbose(f"[server_raw] Used raw string for unknown type: {type(data)}")
|
269
|
-
|
270
|
-
# 检查是否是初始化响应
|
271
|
-
is_init_response = False
|
272
|
-
if isinstance(content, dict):
|
273
|
-
if "result" in content and isinstance(content["result"], dict):
|
274
|
-
result = content["result"]
|
275
|
-
if "protocolVersion" in result or "serverInfo" in result:
|
276
|
-
is_init_response = True
|
277
|
-
verbose(f"[server] 检测到初始化响应: {json.dumps(content)[:200]}...")
|
278
|
-
|
279
|
-
# 特别检查是否包含tools字段,这对于VSCode非常重要
|
280
|
-
if "tools" in result:
|
281
|
-
verbose(f"[server] 检测到tools字段,工具数量: {len(result['tools'])}")
|
282
|
-
|
283
|
-
# 检查数据是否已经是标准的 JSON-RPC 消息
|
284
|
-
if isinstance(content, dict):
|
285
|
-
if "jsonrpc" in content and ("id" in content or "method" in content):
|
286
|
-
# 已经是标准格式,直接输出
|
287
|
-
output = json.dumps(content)
|
288
|
-
verbose(f"[server] Standard JSON-RPC message detected, id: {content.get('id')}")
|
289
|
-
|
290
|
-
elif "result" in content and not "jsonrpc" in content:
|
291
|
-
# 是结果但缺少 jsonrpc 字段,构造标准响应
|
292
|
-
output = json.dumps({
|
293
|
-
"jsonrpc": "2.0",
|
294
|
-
"id": 1, # 默认ID,应该不会被用到
|
295
|
-
"result": content["result"] if "result" in content else content
|
296
|
-
})
|
297
|
-
verbose(f"[server] Fixed response format by adding jsonrpc")
|
298
24
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
# 写入 stdout 并立即刷新,确保 VS Code 能收到
|
308
|
-
sys.stdout.write(output + "\n")
|
309
|
-
sys.stdout.flush()
|
310
|
-
verbose(f"[server] Response sent to stdout: {output}")
|
311
|
-
|
312
|
-
except Exception as e:
|
313
|
-
error(f"[server] Error processing server message: {e}")
|
314
|
-
import traceback
|
315
|
-
error(f"[server] 异常堆栈: {traceback.format_exc()}")
|
316
|
-
# 尝试发送错误响应
|
317
|
-
try:
|
318
|
-
error_resp = json.dumps({
|
319
|
-
"jsonrpc": "2.0",
|
320
|
-
"id": 1, # 使用默认ID
|
321
|
-
"error": {
|
322
|
-
"code": -32603,
|
323
|
-
"message": f"Internal error: {str(e)}"
|
324
|
-
}
|
325
|
-
})
|
326
|
-
sys.stdout.write(error_resp + "\n")
|
327
|
-
sys.stdout.flush()
|
328
|
-
verbose(f"[server] Sent error response due to: {e}")
|
329
|
-
except:
|
330
|
-
error("[server] Failed to send error response")
|
25
|
+
# 导入工具模块
|
26
|
+
from ..utils.process_utils import find_server_process_by_command, save_pid_to_file
|
27
|
+
from ..utils.mcp_utils import (
|
28
|
+
process_client_request,
|
29
|
+
handle_single_server_message,
|
30
|
+
initialize_session
|
31
|
+
)
|
331
32
|
|
332
33
|
|
333
34
|
async def create_stdio_runner(
|
@@ -353,23 +54,54 @@ async def create_stdio_runner(
|
|
353
54
|
verbose("[Runner] Starting cleanup...")
|
354
55
|
is_shutting_down = True
|
355
56
|
try:
|
57
|
+
# Remove PID by server name and client name
|
58
|
+
from ..utils.process_utils import remove_pid_by_server_name
|
59
|
+
|
60
|
+
# 尝试获取第三级进程作为客户端标识 - 与保存时使用相同的逻辑
|
61
|
+
client_name = "unknown_client"
|
62
|
+
try:
|
63
|
+
import psutil
|
64
|
+
current_process = psutil.Process()
|
65
|
+
|
66
|
+
# 收集完整的进程链
|
67
|
+
process_chain = []
|
68
|
+
process = current_process
|
69
|
+
while process:
|
70
|
+
process_chain.append(process.name())
|
71
|
+
process = process.parent()
|
72
|
+
|
73
|
+
# 打印完整进程链用于调试
|
74
|
+
verbose(f"[Runner] 清理时的完整进程链: {' -> '.join(reversed(process_chain))}")
|
75
|
+
|
76
|
+
# 尝试获取第四级进程(如果存在)
|
77
|
+
if len(process_chain) >= 3:
|
78
|
+
client_name = process_chain[2]
|
79
|
+
verbose(f"[Runner] 清理时使用第四级进程作为客户端: {client_name}")
|
80
|
+
else:
|
81
|
+
# 如果进程链不够长,使用最后一个进程(顶层进程)
|
82
|
+
client_name = process_chain[-1] if process_chain else "unknown_client"
|
83
|
+
verbose(f"[Runner] 清理时进程链不够长,使用顶层进程作为客户端: {client_name}")
|
84
|
+
except Exception as e:
|
85
|
+
verbose(f"[Runner] 清理时获取客户端进程名称失败: {e},使用默认值: {client_name}")
|
86
|
+
|
87
|
+
remove_pid_by_server_name(server_details.qualifiedName, client_name)
|
88
|
+
verbose(f"[Runner] 已从PID文件中移除服务器 '{server_details.qualifiedName}' 的客户端 '{client_name}' 记录")
|
89
|
+
|
356
90
|
await exit_stack.aclose()
|
357
91
|
verbose("[Runner] Resources closed successfully")
|
358
92
|
except Exception as error:
|
359
93
|
handle_error(error, "Error during cleanup")
|
360
94
|
verbose("[Runner] Cleanup completed")
|
361
95
|
|
362
|
-
def handle_sigint(
|
96
|
+
async def handle_sigint():
|
363
97
|
verbose("[Runner] Received interrupt signal, shutting down...")
|
364
|
-
|
98
|
+
await cleanup()
|
365
99
|
# 立即打印一条确认消息,让用户知道CTRL+C已被捕获
|
366
|
-
|
100
|
+
verbose("\n[CTRL+C] 正在关闭服务,请稍候...")
|
367
101
|
# 可选:设置一个短暂的超时,然后强制退出
|
368
102
|
import threading
|
369
103
|
threading.Timer(2.0, lambda: os._exit(0)).start()
|
370
104
|
|
371
|
-
signal.signal(signal.SIGINT, handle_sigint)
|
372
|
-
|
373
105
|
# 获取连接配置
|
374
106
|
stdio_connection = next((conn for conn in server_details.connections if conn.type == "stdio"), None)
|
375
107
|
if not stdio_connection:
|
@@ -404,13 +136,64 @@ async def create_stdio_runner(
|
|
404
136
|
async with stdio_client(server_params, errlog=sys.stderr) as (read_stream, write_stream):
|
405
137
|
verbose("Stdio proxy client connection established")
|
406
138
|
|
139
|
+
# 查找服务器进程ID - 优先使用父子进程关系
|
140
|
+
from ..utils.process_utils import find_server_process_by_command, save_pid_to_file, find_server_process_from_current
|
141
|
+
|
142
|
+
# 首先尝试通过父子进程关系查找
|
143
|
+
is_found_from_current, current_pid, cmd_str = find_server_process_from_current()
|
144
|
+
if is_found_from_current and current_pid:
|
145
|
+
verbose(f"[Runner] 通过父子进程关系找到服务器进程,PID: {current_pid}")
|
146
|
+
server_pid = current_pid
|
147
|
+
else:
|
148
|
+
# 如果父子进程关系查找失败,尝试通过命令行查找
|
149
|
+
server_pid = find_server_process_by_command(command, args)
|
150
|
+
|
151
|
+
if server_pid:
|
152
|
+
verbose(f"[Runner] 已找到MCP服务器进程,PID: {server_pid}")
|
153
|
+
|
154
|
+
# 将进程ID写入PID文件
|
155
|
+
# 尝试获取第四级进程作为客户端标识
|
156
|
+
client_name = "unknown_client"
|
157
|
+
try:
|
158
|
+
import psutil
|
159
|
+
current_process = psutil.Process()
|
160
|
+
|
161
|
+
# 收集完整的进程链
|
162
|
+
process_chain = []
|
163
|
+
process = current_process
|
164
|
+
while process:
|
165
|
+
process_chain.append(process.name())
|
166
|
+
process = process.parent()
|
167
|
+
|
168
|
+
# 打印完整进程链用于调试
|
169
|
+
verbose(f"[Runner] 完整进程链: {' -> '.join(reversed(process_chain))}")
|
170
|
+
|
171
|
+
# 尝试获取第四级进程(如果存在)
|
172
|
+
if len(process_chain) >= 3:
|
173
|
+
client_name = process_chain[2]
|
174
|
+
verbose(f"[Runner] 使用第三级进程作为客户端: {client_name}")
|
175
|
+
else:
|
176
|
+
# 如果进程链不够长,使用最后一个进程(顶层进程)
|
177
|
+
client_name = process_chain[-1] if process_chain else "unknown_client"
|
178
|
+
verbose(f"[Runner] 进程链不够长,使用顶层进程作为客户端: {client_name}")
|
179
|
+
except Exception as e:
|
180
|
+
verbose(f"[Runner] 获取客户端进程名称失败: {e},使用默认值: {client_name}")
|
181
|
+
|
182
|
+
verbose(f"[Runner] 使用 '{client_name}' 作为客户端标识")
|
183
|
+
pid_file_path = save_pid_to_file(server_pid, server_details.qualifiedName,
|
184
|
+
client_name, command, args)
|
185
|
+
if pid_file_path:
|
186
|
+
verbose(f"[Runner] 已将服务器PID信息安全地保存到: {pid_file_path}")
|
187
|
+
else:
|
188
|
+
verbose(f"[Runner] 未能找到MCP服务器进程,将无法获取其PID")
|
189
|
+
|
407
190
|
# 创建 MCP 客户端会话
|
408
191
|
from mcp import ClientSession
|
409
192
|
session = await exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
|
410
193
|
|
411
194
|
# 注册消息处理回调
|
412
195
|
def handle_server_message(msg):
|
413
|
-
|
196
|
+
verbose(f"[magenta][server][/magenta] {json.dumps(msg, ensure_ascii=False)}")
|
414
197
|
session.on_message = handle_server_message
|
415
198
|
|
416
199
|
# 初始化 MCP 协议
|
@@ -421,7 +204,11 @@ async def create_stdio_runner(
|
|
421
204
|
verbose("[Runner] 开始处理循环,使用同步阻塞模式")
|
422
205
|
|
423
206
|
# 打印启动消息
|
424
|
-
|
207
|
+
verbose("[cyan]MCP client running. Press Ctrl+C to stop.[/cyan]")
|
208
|
+
|
209
|
+
# 获取事件循环并在主任务中添加信号处理程序
|
210
|
+
loop = asyncio.get_event_loop()
|
211
|
+
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint()))
|
425
212
|
|
426
213
|
# 循环处理客户端请求,直到关闭
|
427
214
|
while not is_shutting_down:
|
@@ -445,8 +232,13 @@ async def create_stdio_runner(
|
|
445
232
|
verbose(f"[stdin] Response sent for method: {method}")
|
446
233
|
else: # 这是通知,不需要响应
|
447
234
|
# 创建通知对象并发送
|
448
|
-
notification_obj = create_request_object(message, method)
|
449
|
-
await session.send_notification(notification_obj)
|
235
|
+
# notification_obj = create_request_object(message, method)
|
236
|
+
# await session.send_notification(notification_obj)
|
237
|
+
await session.send_notification(
|
238
|
+
types.ClientNotification(
|
239
|
+
types.InitializedNotification(method=method)
|
240
|
+
)
|
241
|
+
)
|
450
242
|
verbose(f"[stdin] Notification sent for method: {method}")
|
451
243
|
|
452
244
|
verbose(f"[stdin] Processed: {line.strip()}")
|
@@ -488,7 +280,7 @@ async def create_stdio_runner(
|
|
488
280
|
verbose("[Runner] 处理循环结束")
|
489
281
|
|
490
282
|
except Exception as e:
|
491
|
-
|
283
|
+
verbose(f"[red]Error running stdio proxy: {e}[/red]")
|
492
284
|
raise
|
493
285
|
finally:
|
494
286
|
await cleanup()
|