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.
@@ -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
- from pydantic import BaseModel
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
- else:
300
- # 其他类型的消息,包装为通知
301
- output = json.dumps(content)
302
- verbose("[server] Passing through data as-is")
303
- else:
304
- # 非字典类型,直接序列化
305
- output = json.dumps(content)
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(sig, frame):
96
+ async def handle_sigint():
363
97
  verbose("[Runner] Received interrupt signal, shutting down...")
364
- asyncio.create_task(cleanup())
98
+ await cleanup()
365
99
  # 立即打印一条确认消息,让用户知道CTRL+C已被捕获
366
- print("\n[CTRL+C] 正在关闭服务,请稍候...", flush=True)
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
- rprint(f"[magenta][server][/magenta] {json.dumps(msg, ensure_ascii=False)}")
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
- rprint("[cyan]MCP client running. Press Ctrl+C to stop.[/cyan]")
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
- rprint(f"[red]Error running stdio proxy: {e}[/red]")
283
+ verbose(f"[red]Error running stdio proxy: {e}[/red]")
492
284
  raise
493
285
  finally:
494
286
  await cleanup()