py-mcpdock-cli 1.0.13__py3-none-any.whl → 1.0.17__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.17.dist-info}/METADATA +3 -1
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.17.dist-info}/RECORD +15 -11
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.17.dist-info}/WHEEL +1 -1
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.17.dist-info}/entry_points.txt +0 -0
- {py_mcpdock_cli-1.0.13.dist-info → py_mcpdock_cli-1.0.17.dist-info}/top_level.txt +0 -0
cli/utils/config.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
import json
|
2
|
+
import os
|
3
|
+
from cli.utils.logger import verbose
|
2
4
|
import questionary
|
3
|
-
from rich import print as
|
5
|
+
from rich import print as verbose
|
4
6
|
from typing import Any, Dict, List, Optional, Set, Tuple, TypeVar
|
7
|
+
from ..config.app_config import APP_ENV
|
8
|
+
|
5
9
|
|
6
10
|
# Import proper types from registry module
|
7
11
|
from ..types.registry import (
|
@@ -77,7 +81,7 @@ def format_config_values(
|
|
77
81
|
else:
|
78
82
|
# Re-raise or handle error if conversion fails for a required field
|
79
83
|
# For now, let's add to missing required to indicate an issue
|
80
|
-
|
84
|
+
verbose(f"Warning: Could not convert required value for {property_name}: {e}")
|
81
85
|
missing_required.append(f"{property_name} (invalid format)")
|
82
86
|
|
83
87
|
if missing_required:
|
@@ -176,7 +180,7 @@ async def prompt_for_config_value(
|
|
176
180
|
answer = await prompt_method(message, default=default_value).ask_async()
|
177
181
|
# If it's required and they somehow skip (e.g., Ctrl+C), handle appropriately
|
178
182
|
if is_required and answer is None:
|
179
|
-
|
183
|
+
verbose("[bold red]Required field cannot be skipped.[/bold red]")
|
180
184
|
# Re-prompt or exit - for now, return None which might fail later validation
|
181
185
|
return None
|
182
186
|
return answer
|
@@ -189,14 +193,14 @@ async def prompt_for_config_value(
|
|
189
193
|
)
|
190
194
|
result = await q.ask_async()
|
191
195
|
if result is None and is_required:
|
192
|
-
|
196
|
+
verbose("[bold red]Required field cannot be skipped.[/bold red]")
|
193
197
|
return None # Or raise an error / re-prompt
|
194
198
|
# Convert back if needed (e.g., for numbers, arrays)
|
195
199
|
if result is not None:
|
196
200
|
try:
|
197
201
|
return convert_value_to_type(result, prop_type)
|
198
202
|
except ValueError:
|
199
|
-
|
203
|
+
verbose(
|
200
204
|
f"[yellow]Warning: Could not convert input '{result}' to type '{prop_type}'. Using raw input.[/yellow]")
|
201
205
|
return result # Return raw input if conversion fails after prompt
|
202
206
|
return result # Return None if skipped (and not required)
|
@@ -224,7 +228,7 @@ async def collect_config_values(
|
|
224
228
|
|
225
229
|
required = set(schema.required if schema else [])
|
226
230
|
|
227
|
-
|
231
|
+
verbose("[bold blue]Please provide the following configuration values:[/bold blue]")
|
228
232
|
|
229
233
|
for key, prop_details in env.items():
|
230
234
|
# Skip if value already exists and is not None in the base_config
|
@@ -238,7 +242,7 @@ async def collect_config_values(
|
|
238
242
|
if value is None and key in required:
|
239
243
|
# This case should ideally be handled within prompt_for_config_value
|
240
244
|
# but as a fallback:
|
241
|
-
|
245
|
+
verbose(f"[bold red]Error: Required configuration '{key}' was not provided.[/bold red]")
|
242
246
|
raise ValueError(f"Missing required configuration: {key}")
|
243
247
|
|
244
248
|
# Assign even if None (for optional fields skipped)
|
@@ -248,7 +252,7 @@ async def collect_config_values(
|
|
248
252
|
try:
|
249
253
|
return format_config_values(connection, base_config)
|
250
254
|
except ValueError as e:
|
251
|
-
|
255
|
+
verbose(f"[bold red]Configuration error:[/bold red] {e}")
|
252
256
|
# Return the collected (potentially incomplete/invalid) config
|
253
257
|
# or raise the error depending on desired strictness
|
254
258
|
return base_config
|
@@ -389,14 +393,82 @@ def format_run_config_values(
|
|
389
393
|
# Implementation for formatServerConfig with proxy command support
|
390
394
|
|
391
395
|
|
392
|
-
def
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
396
|
+
def build_dev_config(qualified_name: str,
|
397
|
+
user_config: Dict[str, Any],
|
398
|
+
api_key: Optional[str] = None,
|
399
|
+
config_needed: bool = True):
|
400
|
+
"""
|
401
|
+
Formats server config for development environment.
|
402
|
+
|
403
|
+
Creates a command that runs the Python module with the server name, config and API key,
|
404
|
+
with platform-specific handling for Windows vs Unix-like systems.
|
405
|
+
|
406
|
+
Args:
|
407
|
+
qualified_name: The package identifier
|
408
|
+
user_config: User configuration values
|
409
|
+
api_key: Optional API key
|
410
|
+
config_needed: Whether config flag is needed
|
411
|
+
"""
|
412
|
+
import sys
|
413
|
+
import os
|
414
|
+
|
415
|
+
# 检测是否为Windows系统
|
416
|
+
is_windows = sys.platform == "win32"
|
417
|
+
|
418
|
+
# 使用固定的CLI项目目录(开发环境)
|
419
|
+
cli_dir = os.path.abspath(os.path.join(os.path.dirname(
|
420
|
+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))))))
|
421
|
+
|
422
|
+
if is_windows:
|
423
|
+
# Windows使用cmd作为命令解释器
|
424
|
+
command = "cmd"
|
425
|
+
|
426
|
+
# 构建完整的Windows命令行
|
427
|
+
# 使用 /c 选项指示执行完命令后退出cmd,& 为Windows命令连接符
|
428
|
+
cmd = f"cd /d {cli_dir} & python -m src run {qualified_name}"
|
429
|
+
|
430
|
+
# 添加配置(如果需要)
|
431
|
+
if config_needed and user_config:
|
432
|
+
# Windows中JSON字符串使用双引号包裹,需要转义
|
433
|
+
json_config = json.dumps(user_config).replace('"', '\\"')
|
434
|
+
cmd += f' --config "{json_config}"'
|
435
|
+
|
436
|
+
# 添加 API 密钥(如果有提供)
|
437
|
+
if api_key:
|
438
|
+
cmd += f" --api-key {api_key}"
|
439
|
+
|
440
|
+
# 构建参数列表
|
441
|
+
args = ["/c", cmd]
|
442
|
+
else:
|
443
|
+
# Unix/macOS使用sh作为命令解释器
|
444
|
+
command = "sh"
|
445
|
+
|
446
|
+
# 构建完整的shell命令
|
447
|
+
cmd = f"cd {cli_dir} && python3 -m src run {qualified_name}"
|
448
|
+
|
449
|
+
# 添加配置(如果需要)
|
450
|
+
if config_needed and user_config:
|
451
|
+
# 将配置对象转为JSON字符串,并用单引号包裹(适用于shell命令)
|
452
|
+
json_config = json.dumps(user_config)
|
453
|
+
cmd += f" --config '{json_config}'"
|
454
|
+
|
455
|
+
# 添加 API 密钥(如果有提供)
|
456
|
+
if api_key:
|
457
|
+
cmd += f" --api-key {api_key}"
|
458
|
+
|
459
|
+
# 构建参数列表
|
460
|
+
args = ["-c", cmd]
|
461
|
+
|
462
|
+
return command, args
|
463
|
+
|
464
|
+
|
465
|
+
def build_prod_config(qualified_name: str,
|
466
|
+
user_config: Dict[str, Any],
|
467
|
+
api_key: Optional[str] = None,
|
468
|
+
config_needed: bool = True):
|
398
469
|
"""
|
399
470
|
Formats server config into a command structure with proxy command support.
|
471
|
+
Cross-platform compatible for both Windows and Unix-like systems.
|
400
472
|
|
401
473
|
Args:
|
402
474
|
qualified_name: The package identifier (被代理的命令标识)
|
@@ -405,10 +477,21 @@ def format_server_config(
|
|
405
477
|
config_needed: Whether config flag is needed
|
406
478
|
|
407
479
|
Returns:
|
408
|
-
|
480
|
+
Tuple containing command and args
|
409
481
|
"""
|
410
|
-
|
411
|
-
|
482
|
+
import sys
|
483
|
+
|
484
|
+
# 检测是否为Windows系统
|
485
|
+
is_windows = sys.platform == "win32"
|
486
|
+
|
487
|
+
# 根据系统选择合适的包管理工具命令
|
488
|
+
if is_windows:
|
489
|
+
# Windows下更常用npx而非uvx
|
490
|
+
command = "npx"
|
491
|
+
else:
|
492
|
+
# Unix-like系统使用uvx
|
493
|
+
command = "uvx"
|
494
|
+
|
412
495
|
proxy_package = "@mcpspace/proxy" # 默认代理包
|
413
496
|
|
414
497
|
# 构建参数列表
|
@@ -424,16 +507,32 @@ def format_server_config(
|
|
424
507
|
args.append(qualified_name)
|
425
508
|
|
426
509
|
# 添加配置(如果需要)
|
427
|
-
|
428
|
-
|
429
|
-
|
510
|
+
if config_needed and user_config:
|
511
|
+
args.append("--config")
|
512
|
+
# 在Windows上,可能需要特别处理JSON字符串中的引号
|
513
|
+
json_config = json.dumps(user_config)
|
514
|
+
if is_windows:
|
515
|
+
json_config = json_config.replace('"', '\\"')
|
516
|
+
args.append(json_config)
|
430
517
|
|
431
518
|
# 添加 API 密钥(如果有提供)
|
432
519
|
if api_key:
|
433
520
|
args.append("--api-key")
|
434
521
|
args.append(api_key)
|
522
|
+
return command, args
|
523
|
+
|
435
524
|
|
436
|
-
|
525
|
+
def format_server_config(
|
526
|
+
qualified_name: str,
|
527
|
+
user_config: Dict[str, Any],
|
528
|
+
api_key: Optional[str] = None,
|
529
|
+
config_needed: bool = True,
|
530
|
+
) -> ConfiguredServer:
|
531
|
+
command, args = None, None
|
532
|
+
if APP_ENV != 'dev':
|
533
|
+
command, args = build_prod_config(qualified_name, user_config, api_key, config_needed)
|
534
|
+
else:
|
535
|
+
command, args = build_dev_config(qualified_name, user_config, api_key, config_needed)
|
437
536
|
return ConfiguredServer(
|
438
537
|
command=command,
|
439
538
|
args=args,
|
cli/utils/logger.py
CHANGED
@@ -60,10 +60,10 @@ if not cli_logger.handlers:
|
|
60
60
|
|
61
61
|
# Console handler: 仅在开发环境或DEBUG_STDIO_RUNNER=1时输出到终端
|
62
62
|
# if APP_ENV == "dev":
|
63
|
-
ch_cli = logging.StreamHandler()
|
64
|
-
ch_cli.setLevel(logging.DEBUG)
|
65
|
-
ch_cli.setFormatter(CustomFormatter())
|
66
|
-
cli_logger.addHandler(ch_cli)
|
63
|
+
# ch_cli = logging.StreamHandler()
|
64
|
+
# ch_cli.setLevel(logging.DEBUG)
|
65
|
+
# ch_cli.setFormatter(CustomFormatter())
|
66
|
+
# cli_logger.addHandler(ch_cli)
|
67
67
|
|
68
68
|
|
69
69
|
def verbose(message: str) -> None:
|
cli/utils/mcp_utils.py
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
"""
|
2
|
+
MCP 协议请求/响应处理工具模块
|
3
|
+
|
4
|
+
该模块提供了处理 MCP 协议请求和响应的工具函数,包括:
|
5
|
+
- 创建请求对象
|
6
|
+
- 发送请求并处理超时
|
7
|
+
- 处理服务器消息
|
8
|
+
"""
|
9
|
+
import json
|
10
|
+
import anyio
|
11
|
+
import traceback
|
12
|
+
from typing import Dict, Any, Optional
|
13
|
+
from logging import error
|
14
|
+
import sys
|
15
|
+
|
16
|
+
from ..utils.logger import verbose
|
17
|
+
from mcp.types import ClientRequest, ServerRequest
|
18
|
+
from pydantic import BaseModel
|
19
|
+
|
20
|
+
# 定义一个通用的 Model,用于接收任何 JSON 响应
|
21
|
+
|
22
|
+
|
23
|
+
class DictModel(BaseModel):
|
24
|
+
@classmethod
|
25
|
+
def model_validate(cls, value):
|
26
|
+
if isinstance(value, dict):
|
27
|
+
return value
|
28
|
+
return dict(value)
|
29
|
+
|
30
|
+
|
31
|
+
def create_request_object(message: Dict[str, Any], method: str):
|
32
|
+
"""
|
33
|
+
根据方法类型创建适当的请求对象
|
34
|
+
|
35
|
+
Args:
|
36
|
+
message: JSON-RPC 消息
|
37
|
+
method: 请求方法名
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
MCP 请求对象
|
41
|
+
"""
|
42
|
+
# 作为代理,我们不需要严格验证方法是否符合标准列表
|
43
|
+
# 直接创建ClientRequest对象,透明转发所有请求
|
44
|
+
msg = dict(message)
|
45
|
+
msg.pop("jsonrpc", None) # 移除 jsonrpc 字段,SDK会自动添加
|
46
|
+
msg.pop("id", None) # 移除 id 字段,我们会在响应中重新添加
|
47
|
+
|
48
|
+
try:
|
49
|
+
# 尝试创建 ClientRequest
|
50
|
+
return ClientRequest(**msg)
|
51
|
+
except Exception as e:
|
52
|
+
verbose('---------------------------------------')
|
53
|
+
verbose(msg)
|
54
|
+
verbose('---------------------------------------')
|
55
|
+
# 如果创建失败,记录详细错误信息并回退到 ServerRequest
|
56
|
+
verbose(f"[Runner] 创建 ClientRequest 失败,错误信息: {str(e)},回退到 ServerRequest")
|
57
|
+
return ServerRequest(method=method, params=message.get("params", {}))
|
58
|
+
|
59
|
+
|
60
|
+
async def send_request_with_timeout(session, req_obj, original_id, timeout_seconds=60):
|
61
|
+
"""
|
62
|
+
发送请求并处理超时和错误情况
|
63
|
+
|
64
|
+
Args:
|
65
|
+
session: MCP 客户端会话
|
66
|
+
req_obj: 请求对象
|
67
|
+
original_id: 原始请求ID
|
68
|
+
timeout_seconds: 超时时间(秒)
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
JSON-RPC 响应字符串
|
72
|
+
"""
|
73
|
+
try:
|
74
|
+
# 初始化 resp 为 None,防止超时时未定义
|
75
|
+
resp = None
|
76
|
+
# 使用超时机制
|
77
|
+
with anyio.move_on_after(timeout_seconds):
|
78
|
+
# 记录请求信息
|
79
|
+
verbose(f"[Runner] 发送请求,原始ID={original_id}, 方法={req_obj.method if hasattr(req_obj, 'method') else '未知'}")
|
80
|
+
|
81
|
+
# 发送请求并等待响应
|
82
|
+
resp = await session.send_request(req_obj, DictModel)
|
83
|
+
|
84
|
+
if resp:
|
85
|
+
# 直接使用原始ID构造响应
|
86
|
+
return json.dumps({
|
87
|
+
"id": original_id,
|
88
|
+
"jsonrpc": "2.0",
|
89
|
+
"result": resp
|
90
|
+
})
|
91
|
+
else:
|
92
|
+
return json.dumps({
|
93
|
+
"id": original_id,
|
94
|
+
"jsonrpc": "2.0",
|
95
|
+
"error": {
|
96
|
+
"code": -32000,
|
97
|
+
"message": "Request timed out or empty response"
|
98
|
+
}
|
99
|
+
})
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
# 处理请求错误
|
103
|
+
error_msg = str(e)
|
104
|
+
error_code = -32603
|
105
|
+
|
106
|
+
# 分类错误类型
|
107
|
+
if "timed out" in error_msg.lower():
|
108
|
+
error_code = -32001
|
109
|
+
elif "connection" in error_msg.lower():
|
110
|
+
error_code = -32002
|
111
|
+
|
112
|
+
return json.dumps({
|
113
|
+
"id": original_id,
|
114
|
+
"jsonrpc": "2.0",
|
115
|
+
"error": {
|
116
|
+
"code": error_code,
|
117
|
+
"message": f"Request failed: {error_msg}"
|
118
|
+
}
|
119
|
+
})
|
120
|
+
|
121
|
+
|
122
|
+
async def process_client_request(message, session):
|
123
|
+
"""
|
124
|
+
处理从客户端接收的请求并转发到服务器
|
125
|
+
|
126
|
+
Args:
|
127
|
+
message: 客户端请求消息
|
128
|
+
session: MCP 客户端会话
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
响应字符串
|
132
|
+
"""
|
133
|
+
original_id = message.get("id")
|
134
|
+
method = message.get("method")
|
135
|
+
params = message.get("params", {})
|
136
|
+
|
137
|
+
# 添加特殊处理,记录初始化请求的详细信息
|
138
|
+
if method == "initialize":
|
139
|
+
verbose(f"[Runner] 收到初始化请求 ID: {original_id}, 详细内容: {json.dumps(message)}")
|
140
|
+
|
141
|
+
# 对初始化请求使用SDK的initialize方法,而不是简单转发
|
142
|
+
try:
|
143
|
+
verbose("[Runner] 使用SDK的initialize方法处理初始化请求")
|
144
|
+
# 先创建正确的请求对象
|
145
|
+
req_obj = create_request_object(message, method)
|
146
|
+
|
147
|
+
# 发送实际的初始化请求到下游服务器
|
148
|
+
verbose("[Runner] 向下游服务器发送初始化请求")
|
149
|
+
init_result = await session.send_request(req_obj, DictModel)
|
150
|
+
verbose(
|
151
|
+
f"[Runner] 收到下游服务器初始化响应: {json.dumps(init_result)[:200] if isinstance(init_result, dict) else str(init_result)[:200]}...")
|
152
|
+
|
153
|
+
# 确保我们有一个有效的响应
|
154
|
+
if init_result is None:
|
155
|
+
verbose("[Runner] 收到空的初始化响应,创建默认响应")
|
156
|
+
init_result = {
|
157
|
+
"protocolVersion": "2024-11-05",
|
158
|
+
"serverInfo": {
|
159
|
+
"name": "mcpy-proxy",
|
160
|
+
"version": "1.0.0"
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
# 构造完整的JSON-RPC响应
|
165
|
+
response = json.dumps({
|
166
|
+
"jsonrpc": "2.0",
|
167
|
+
"id": original_id,
|
168
|
+
"result": init_result
|
169
|
+
})
|
170
|
+
|
171
|
+
verbose(f"[Runner] 构造的初始化响应: {response[:200]}...")
|
172
|
+
return response
|
173
|
+
except Exception as e:
|
174
|
+
error(f"[Runner] 初始化请求处理失败: {str(e)}")
|
175
|
+
error(f"[Runner] 异常堆栈: {traceback.format_exc()}")
|
176
|
+
# 如果处理失败,回退到常规请求处理
|
177
|
+
verbose("[Runner] 回退到常规请求处理方式")
|
178
|
+
|
179
|
+
verbose(f"[stdin] Processing request with id: {original_id}, method: {method}")
|
180
|
+
|
181
|
+
# 常规请求处理:确定请求类型和构建请求对象
|
182
|
+
req_obj = create_request_object(message, method)
|
183
|
+
|
184
|
+
# 发送请求并处理响应
|
185
|
+
try:
|
186
|
+
verbose(f"[Runner] 向下游服务器发送请求,method: {method}, id: {original_id}")
|
187
|
+
result = await send_request_with_timeout(session, req_obj, original_id)
|
188
|
+
|
189
|
+
if method == "initialize":
|
190
|
+
verbose(f"[Runner] 收到初始化响应: {result}")
|
191
|
+
|
192
|
+
return result
|
193
|
+
except Exception as e:
|
194
|
+
error(f"[Runner] 请求处理异常 ({method}): {str(e)}")
|
195
|
+
raise
|
196
|
+
|
197
|
+
|
198
|
+
async def handle_single_server_message(data):
|
199
|
+
"""
|
200
|
+
处理单个从服务器接收的消息并输出到标准输出
|
201
|
+
|
202
|
+
Args:
|
203
|
+
data: 服务器消息数据
|
204
|
+
"""
|
205
|
+
try:
|
206
|
+
# 记录接收到的原始数据类型,帮助调试
|
207
|
+
verbose(f"[server_raw] Received data type: {type(data)}")
|
208
|
+
|
209
|
+
# 尝试获取原始数据的字符串表示用于调试
|
210
|
+
raw_data_str = str(data)
|
211
|
+
if len(raw_data_str) > 500:
|
212
|
+
raw_data_str = raw_data_str[:500] + "..."
|
213
|
+
verbose(f"[server_raw] 原始数据: {raw_data_str}")
|
214
|
+
|
215
|
+
# 根据数据类型进行处理
|
216
|
+
if hasattr(data, "model_dump"):
|
217
|
+
content = data.model_dump()
|
218
|
+
verbose(f"[server_raw] Processed pydantic v2 model: {type(data)}")
|
219
|
+
elif hasattr(data, "dict"):
|
220
|
+
content = data.dict()
|
221
|
+
verbose(f"[server_raw] Processed pydantic v1 model: {type(data)}")
|
222
|
+
elif isinstance(data, dict):
|
223
|
+
content = data
|
224
|
+
verbose(f"[server_raw] Processed dict with {len(data)} keys")
|
225
|
+
else:
|
226
|
+
# 尝试转换为字符串,然后解析为JSON
|
227
|
+
try:
|
228
|
+
content = json.loads(str(data))
|
229
|
+
verbose(f"[server_raw] Converted to JSON: {type(data)}")
|
230
|
+
except:
|
231
|
+
content = {"data": str(data)}
|
232
|
+
verbose(f"[server_raw] Used raw string for unknown type: {type(data)}")
|
233
|
+
|
234
|
+
# 检查是否是初始化响应
|
235
|
+
is_init_response = False
|
236
|
+
if isinstance(content, dict):
|
237
|
+
if "result" in content and isinstance(content["result"], dict):
|
238
|
+
result = content["result"]
|
239
|
+
if "protocolVersion" in result or "serverInfo" in result:
|
240
|
+
is_init_response = True
|
241
|
+
verbose(f"[server] 检测到初始化响应: {json.dumps(content)[:200]}...")
|
242
|
+
|
243
|
+
# 特别检查是否包含tools字段,这对于VSCode非常重要
|
244
|
+
if "tools" in result:
|
245
|
+
verbose(f"[server] 检测到tools字段,工具数量: {len(result['tools'])}")
|
246
|
+
|
247
|
+
# 检查数据是否已经是标准的 JSON-RPC 消息
|
248
|
+
if isinstance(content, dict):
|
249
|
+
if "jsonrpc" in content and ("id" in content or "method" in content):
|
250
|
+
# 已经是标准格式,直接输出
|
251
|
+
output = json.dumps(content)
|
252
|
+
verbose(f"[server] Standard JSON-RPC message detected, id: {content.get('id')}")
|
253
|
+
|
254
|
+
elif "result" in content and not "jsonrpc" in content:
|
255
|
+
# 是结果但缺少 jsonrpc 字段,构造标准响应
|
256
|
+
output = json.dumps({
|
257
|
+
"jsonrpc": "2.0",
|
258
|
+
"id": 1, # 默认ID,应该不会被用到
|
259
|
+
"result": content["result"] if "result" in content else content
|
260
|
+
})
|
261
|
+
verbose(f"[server] Fixed response format by adding jsonrpc")
|
262
|
+
|
263
|
+
else:
|
264
|
+
# 其他类型的消息,包装为通知
|
265
|
+
output = json.dumps(content)
|
266
|
+
verbose("[server] Passing through data as-is")
|
267
|
+
else:
|
268
|
+
# 非字典类型,直接序列化
|
269
|
+
output = json.dumps(content)
|
270
|
+
|
271
|
+
# 写入 stdout 并立即刷新,确保 VS Code 能收到
|
272
|
+
sys.stdout.write(output + "\n")
|
273
|
+
sys.stdout.flush()
|
274
|
+
verbose(f"[server] Response sent to stdout: {output}")
|
275
|
+
|
276
|
+
except Exception as e:
|
277
|
+
error(f"[server] Error processing server message: {e}")
|
278
|
+
error(f"[server] 异常堆栈: {traceback.format_exc()}")
|
279
|
+
# 尝试发送错误响应
|
280
|
+
try:
|
281
|
+
error_resp = json.dumps({
|
282
|
+
"jsonrpc": "2.0",
|
283
|
+
"id": 1, # 使用默认ID
|
284
|
+
"error": {
|
285
|
+
"code": -32603,
|
286
|
+
"message": f"Internal error: {str(e)}"
|
287
|
+
}
|
288
|
+
})
|
289
|
+
sys.stdout.write(error_resp + "\n")
|
290
|
+
sys.stdout.flush()
|
291
|
+
verbose(f"[server] Sent error response due to: {e}")
|
292
|
+
except:
|
293
|
+
error("[server] Failed to send error response")
|
294
|
+
|
295
|
+
|
296
|
+
async def initialize_session(session):
|
297
|
+
"""
|
298
|
+
初始化 MCP 协议会话,转发上游客户端的初始化请求到下游服务器
|
299
|
+
|
300
|
+
Args:
|
301
|
+
session: MCP 客户端会话
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
初始化是否成功
|
305
|
+
"""
|
306
|
+
try:
|
307
|
+
# 关键点: 作为代理,我们不应该主动调用 session.initialize()
|
308
|
+
# 上游客户端会发送初始化请求,我们应该在 handle_stdin 函数中处理
|
309
|
+
verbose("[Runner] MCP 代理准备就绪,等待上游客户端的初始化请求...")
|
310
|
+
return True
|
311
|
+
except Exception as init_error:
|
312
|
+
error_msg = f"代理初始化失败: {str(init_error)}"
|
313
|
+
error(f"[Runner] {error_msg}")
|
314
|
+
return False
|