py-mcpdock-cli 1.0.13__tar.gz → 1.0.18__tar.gz

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.
Files changed (41) hide show
  1. {py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info → py_mcpdock_cli-1.0.18}/PKG-INFO +3 -1
  2. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/pyproject.toml +4 -2
  3. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/commands/install.py +78 -81
  4. py_mcpdock_cli-1.0.18/src/cli/commands/query_pid.py +36 -0
  5. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/commands/run.py +9 -11
  6. py_mcpdock_cli-1.0.18/src/cli/main.py +16 -0
  7. py_mcpdock_cli-1.0.18/src/cli/runners/stdio_runner.py +286 -0
  8. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/utils/config.py +120 -21
  9. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/utils/logger.py +4 -4
  10. py_mcpdock_cli-1.0.18/src/cli/utils/mcp_utils.py +314 -0
  11. py_mcpdock_cli-1.0.18/src/cli/utils/process_utils.py +656 -0
  12. py_mcpdock_cli-1.0.18/src/cli/utils/run_query_pid_by_packagename.py +62 -0
  13. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18/src/py_mcpdock_cli.egg-info}/PKG-INFO +3 -1
  14. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/py_mcpdock_cli.egg-info/SOURCES.txt +4 -0
  15. py_mcpdock_cli-1.0.18/src/py_mcpdock_cli.egg-info/entry_points.txt +2 -0
  16. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/py_mcpdock_cli.egg-info/requires.txt +2 -0
  17. py_mcpdock_cli-1.0.18/src/py_mcpdock_cli.egg-info/top_level.txt +2 -0
  18. py_mcpdock_cli-1.0.13/src/cli/main.py +0 -12
  19. py_mcpdock_cli-1.0.13/src/cli/runners/stdio_runner.py +0 -494
  20. py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/entry_points.txt +0 -2
  21. py_mcpdock_cli-1.0.13/src/py_mcpdock_cli.egg-info/top_level.txt +0 -1
  22. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/MANIFEST.in +0 -0
  23. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/README.md +0 -0
  24. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/setup.cfg +0 -0
  25. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/__init__.py +0 -0
  26. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/commands/__init__.py +0 -0
  27. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/config/__init__.py +0 -0
  28. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/config/app_config.py +0 -0
  29. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/config/client_config.py +0 -0
  30. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/mock_servers.json +0 -0
  31. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/registry.py +0 -0
  32. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/runners/__init__.py +0 -0
  33. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/runners/command_runner.py +0 -0
  34. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/runners/stream_http_runner.py +0 -0
  35. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/runners/ws_runner.py +0 -0
  36. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/types/__init__.py +0 -0
  37. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/types/registry.py +0 -0
  38. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/utils/__init__.py +0 -0
  39. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/utils/client.py +0 -0
  40. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/cli/utils/runtime.py +0 -0
  41. {py_mcpdock_cli-1.0.13 → py_mcpdock_cli-1.0.18}/src/py_mcpdock_cli.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-mcpdock-cli
3
- Version: 1.0.13
3
+ Version: 1.0.18
4
4
  Summary: Python CLI for managing MCP servers
5
5
  Author-email: dw <qindongwoxin@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/yourusername/py-mcpdock-cli
@@ -21,6 +21,8 @@ Requires-Dist: asyncio>=3.4.3
21
21
  Requires-Dist: python-dotenv>=1.0.0
22
22
  Requires-Dist: mcp
23
23
  Requires-Dist: aiohttp>=3.8.0
24
+ Requires-Dist: psutil>=5.9.0
25
+ Requires-Dist: filelock>=3.18.0
24
26
 
25
27
  # MCP CLI Python Implementation
26
28
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "py-mcpdock-cli"
7
- version = "1.0.13"
7
+ version = "1.0.18"
8
8
  authors = [
9
9
  {name = "dw", email = "qindongwoxin@gmail.com"},
10
10
  ]
@@ -28,13 +28,15 @@ dependencies = [
28
28
  "python-dotenv>=1.0.0",
29
29
  "mcp",
30
30
  "aiohttp>=3.8.0",
31
+ "psutil>=5.9.0",
32
+ "filelock>=3.18.0",
31
33
  ]
32
34
 
33
35
  [tool.setuptools.packages.find]
34
36
  where = ["src"]
35
37
 
36
38
  [project.scripts]
37
- mcpy = "cli.main:cli"
39
+ mcpdock = "cli.main:cli"
38
40
 
39
41
  [project.urls]
40
42
  "Homepage" = "https://github.com/yourusername/py-mcpdock-cli"
@@ -77,84 +77,81 @@ async def install_mcp_server(
77
77
  target_client = target_client or "claude" # Default to Claude if no client specified
78
78
  verbose(f"Initiating installation of {package_identifier} for {target_client}")
79
79
 
80
- console = Console()
81
-
82
80
  # Create and start spinner for package resolution
83
- with console.status(f"Resolving {package_identifier}...", spinner="dots") as status:
84
- try:
85
- # Resolve the package (fetch server metadata)
86
- verbose(f"Resolving package: {package_identifier}")
87
- server_data = await resolve_package(package_identifier)
88
- verbose(f"Package resolved successfully: {server_data.qualifiedName}")
89
-
90
- # Choose the appropriate connection type
91
- verbose("Choosing connection type...")
92
- connection = choose_connection(server_data)
93
- verbose(f"Selected connection type: {connection.type}")
94
-
95
- # Check for required runtimes and install if needed
96
- # Commented out as these are specific to JS environment
97
- # await ensure_uv_installed(connection)
98
- # await ensure_bun_installed(connection)
99
-
100
- # Inform users of remote server installation if applicable
101
- is_remote = check_and_notify_remote_server(server_data)
102
- if is_remote:
103
- verbose("Remote server detected, notification displayed")
104
-
105
- # Get the validated config values, no prompting
106
- status.update(f"Validating configuration for {package_identifier}...")
107
- collected_config_values = await validate_config_values(connection, initial_config)
108
-
109
- # Determine if we need to pass config flag
110
- config_flag_needed = initial_config is not None
111
-
112
- verbose(f"Config values validated: {json.dumps(collected_config_values, indent=2)}")
113
- verbose(f"Using config flag: {config_flag_needed}")
114
-
115
- # Format the server configuration
116
- verbose("Formatting server configuration...")
117
- server_config = format_server_config(
118
- package_identifier,
119
- collected_config_values,
120
- api_key,
121
- config_flag_needed,
122
- )
123
- verbose(f"Formatted server config: {json.dumps(server_config.__dict__, indent=2)}")
124
-
125
- # Read existing config from client
126
- status.update(f"Installing for {target_client}...")
127
- verbose(f"Reading configuration for client: {target_client}")
128
- client_config = read_config(target_client)
129
-
130
- # Get normalized server name to use as key
131
- server_name = get_server_name(package_identifier)
132
- verbose(f"Normalized server ID: {server_name}")
133
-
134
- # Update client configuration with new server
135
- verbose("Updating client configuration...")
136
- if not isinstance(client_config.mcpServers, dict):
137
- # Initialize if needed
138
- client_config.mcpServers = {}
139
-
140
- # Add the new server config
141
- client_config.mcpServers[server_name] = server_config
142
-
143
- # Write updated configuration
144
- verbose("Writing updated configuration...")
145
- write_config(client_config, target_client)
146
- verbose("Configuration successfully written")
147
-
148
- rprint(f"[green]{package_identifier} successfully installed for {target_client}[/green]")
149
-
150
- # No prompt for client restart
151
- verbose("Installation completed successfully")
152
-
153
- except Exception as e:
154
- verbose(f"Installation error: {str(e)}")
155
- rprint(f"[red]Failed to install {package_identifier}[/red]")
156
- rprint(f"[red]Error: {str(e)}[/red]")
157
- raise
81
+ try:
82
+ # Resolve the package (fetch server metadata)
83
+ verbose(f"Resolving package: {package_identifier}")
84
+ server_data = await resolve_package(package_identifier)
85
+ verbose(f"Package resolved successfully: {server_data.qualifiedName}")
86
+
87
+ # Choose the appropriate connection type
88
+ verbose("Choosing connection type...")
89
+ connection = choose_connection(server_data)
90
+ verbose(f"Selected connection type: {connection.type}")
91
+
92
+ # Check for required runtimes and install if needed
93
+ # Commented out as these are specific to JS environment
94
+ # await ensure_uv_installed(connection)
95
+ # await ensure_bun_installed(connection)
96
+
97
+ # Inform users of remote server installation if applicable
98
+ is_remote = check_and_notify_remote_server(server_data)
99
+ if is_remote:
100
+ verbose("Remote server detected, notification displayed")
101
+
102
+ # Get the validated config values, no prompting
103
+ verbose(f"Validating configuration for {package_identifier}...")
104
+ collected_config_values = await validate_config_values(connection, initial_config)
105
+
106
+ # Determine if we need to pass config flag
107
+ config_flag_needed = initial_config is not None
108
+
109
+ verbose(f"Config values validated: {json.dumps(collected_config_values, indent=2)}")
110
+ verbose(f"Using config flag: {config_flag_needed}")
111
+
112
+ # Format the server configuration
113
+ verbose("Formatting server configuration...")
114
+ server_config = format_server_config(
115
+ package_identifier,
116
+ collected_config_values,
117
+ api_key,
118
+ config_flag_needed,
119
+ )
120
+ verbose(f"Formatted server config: {json.dumps(server_config.__dict__, indent=2)}")
121
+
122
+ # Read existing config from client
123
+ verbose(f"Installing for {target_client}...")
124
+ verbose(f"Reading configuration for client: {target_client}")
125
+ client_config = read_config(target_client)
126
+
127
+ # Get normalized server name to use as key
128
+ server_name = get_server_name(package_identifier)
129
+ verbose(f"Normalized server ID: {server_name}")
130
+
131
+ # Update client configuration with new server
132
+ verbose("Updating client configuration...")
133
+ if not isinstance(client_config.mcpServers, dict):
134
+ # Initialize if needed
135
+ client_config.mcpServers = {}
136
+
137
+ # Add the new server config
138
+ client_config.mcpServers[server_name] = server_config
139
+
140
+ # Write updated configuration
141
+ verbose("Writing updated configuration...")
142
+ write_config(client_config, target_client)
143
+ verbose("Configuration successfully written")
144
+
145
+ rprint(f"[green]{package_identifier} successfully installed for {target_client}[/green]")
146
+
147
+ # No prompt for client restart
148
+ verbose("Installation completed successfully")
149
+
150
+ except Exception as e:
151
+ verbose(f"Installation error: {str(e)}")
152
+ rprint(f"[red]Failed to install {package_identifier}[/red]")
153
+ rprint(f"[red]Error: {str(e)}[/red]")
154
+ raise
158
155
 
159
156
 
160
157
  @click.command("install")
@@ -164,19 +161,19 @@ async def install_mcp_server(
164
161
  @click.option("--api-key", type=click.STRING, help="Provide an API key for fetching saved configurations")
165
162
  def install(mcp_server: str, client: Optional[str], env_json: Optional[str], api_key: Optional[str]):
166
163
  """Install an AI mcp-server with optional configuration."""
167
- click.echo(f"Attempting to install {mcp_server}...")
164
+ verbose(f"Attempting to install {mcp_server}...")
168
165
  user_config = None
169
166
  if env_json:
170
167
  try:
171
168
  user_config = json.loads(env_json)
172
- click.echo(f"Using provided config: {user_config}")
169
+ verbose(f"Using provided config: {user_config}")
173
170
  except json.JSONDecodeError as e:
174
- click.echo(f"Error: Invalid JSON provided for --config: {e}", err=True)
171
+ verbose(f"Error: Invalid JSON provided for --config: {e}", err=True)
175
172
  return
176
173
 
177
174
  try:
178
175
  # Run the async installation process
179
176
  asyncio.run(install_mcp_server(mcp_server, client, user_config, api_key))
180
177
  except Exception as e:
181
- click.echo(f"Installation failed: {str(e)}", err=True)
178
+ verbose(f"Installation failed: {str(e)}", err=True)
182
179
  return 1
@@ -0,0 +1,36 @@
1
+ from cli.utils.logger import verbose
2
+ import click
3
+ import json
4
+ import sys
5
+ from ..utils.run_query_pid_by_packagename import query_pid_by_packagename
6
+
7
+
8
+ @click.command("query-pid")
9
+ @click.argument("package_names", type=click.STRING, nargs=-1, required=True)
10
+ @click.option("--json-output", is_flag=True, help="Output results in JSON format")
11
+ def query_pid(package_names, json_output):
12
+ """
13
+ Check if the processes for the specified package names are running.
14
+
15
+ PACKAGE_NAMES: One or more package names to check.
16
+ Returns the running status for each package name. If the --json-output option is used, outputs the result in JSON format.
17
+ """
18
+ try:
19
+ # Get the running status for each package name
20
+ status_dict = query_pid_by_packagename(list(package_names))
21
+
22
+ if json_output:
23
+ # Output results in JSON format
24
+ print(json.dumps(status_dict))
25
+ else:
26
+ # Output results in plain text format
27
+ for package_name, is_running in status_dict.items():
28
+ status = "Running" if is_running else "Not running"
29
+ verbose(f"Package name '{package_name}': {status}")
30
+
31
+ # Check if any process is running
32
+ any_running = any(status_dict.values())
33
+ exit(0 if any_running else 1)
34
+ except Exception as e:
35
+ print(f"Error occurred while executing query-pid command: {e}", file=sys.stderr)
36
+ exit(1) # Return 1 to indicate an error
@@ -3,7 +3,7 @@ import json
3
3
  import asyncio
4
4
  from typing import Dict, Any, Optional
5
5
 
6
- from rich import print as rprint
6
+ from rich import print as verbose
7
7
  from rich.console import Console
8
8
 
9
9
  from ..utils.logger import verbose
@@ -91,8 +91,8 @@ async def run_server(
91
91
  # Inform about remote server if applicable
92
92
  # check_and_notify_remote_server(resolved_server)
93
93
 
94
- rprint(f"[blue][Runner] Connecting to server:[/blue] {resolved_server.qualifiedName}")
95
- rprint(f"Connection types: {[c.type for c in resolved_server.connections]}")
94
+ verbose(f"[blue][Runner] Connecting to server:[/blue] {resolved_server.qualifiedName}")
95
+ verbose(f"Connection types: {[c.type for c in resolved_server.connections]}")
96
96
 
97
97
  # Assume analytics is disabled for now
98
98
  analytics_enabled = False
@@ -105,7 +105,7 @@ async def run_server(
105
105
  analytics_enabled
106
106
  )
107
107
  except Exception as e:
108
- rprint(f"[red][Runner] Fatal error:[/red] {str(e)}")
108
+ verbose(f"[red][Runner] Fatal error:[/red] {str(e)}")
109
109
  raise
110
110
 
111
111
 
@@ -115,18 +115,18 @@ async def run_server(
115
115
  @click.option("--api-key", type=click.STRING, help="API key for retrieving saved configuration")
116
116
  def run(mcp_server: str, config_json: Optional[str], api_key: Optional[str]):
117
117
  """Run an AI MCP server."""
118
- click.echo(f"Attempting to run {mcp_server}...")
118
+ verbose(f"Attempting to run {mcp_server}...")
119
119
  server_config = None
120
120
 
121
121
  # Parse command line provided configuration
122
122
  if config_json:
123
123
  try:
124
+ verbose(config_json)
124
125
  server_config = json.loads(config_json)
125
- click.echo(f"Using provided config: {server_config}")
126
+ verbose(f"Using provided config: {server_config}")
126
127
  except json.JSONDecodeError as e:
127
- click.echo(f"Error: Invalid JSON provided for --config: {e}", err=True)
128
+ verbose(f"Error: Invalid JSON provided for --config: {e}")
128
129
  return 1
129
-
130
130
  # If no config provided, use empty dict
131
131
  if server_config is None:
132
132
  verbose("No config provided, running with empty config")
@@ -135,8 +135,6 @@ def run(mcp_server: str, config_json: Optional[str], api_key: Optional[str]):
135
135
  if api_key:
136
136
  verbose(f"API key provided: {'*' * len(api_key)}")
137
137
 
138
- console = Console()
139
-
140
138
  try:
141
139
  # with console.status(f"Starting {mcp_server}...", spinner="dots") as status:
142
140
  # Run the server asynchronously
@@ -144,5 +142,5 @@ def run(mcp_server: str, config_json: Optional[str], api_key: Optional[str]):
144
142
  asyncio.run(run_server(mcp_server, server_config, api_key))
145
143
  # status.update(f"Successfully started {mcp_server}")
146
144
  except Exception as e:
147
- click.echo(f"Run failed: {str(e)}", err=True)
145
+ verbose(f"Run failed: {str(e)}")
148
146
  return 1
@@ -0,0 +1,16 @@
1
+ import click
2
+ from .commands.install import install
3
+ from .commands.run import run
4
+ from .commands.query_pid import query_pid
5
+
6
+
7
+ @click.group()
8
+ @click.version_option(version="1.0.0", package_name="py-cli") # Corresponds to program.version("1.0.0")
9
+ def cli():
10
+ """A custom CLI tool translated to Python."""
11
+ pass
12
+
13
+
14
+ cli.add_command(install)
15
+ cli.add_command(run)
16
+ cli.add_command(query_pid)
@@ -0,0 +1,286 @@
1
+ """
2
+ STDIO Runner implementation for local MCP servers
3
+
4
+ This module provides functionality for creating and managing a connection with an MCP server
5
+ through standard input/output pipes.
6
+ """
7
+ import json
8
+ import asyncio
9
+ from logging import error
10
+ import mcp.types as types
11
+ import signal
12
+ import sys
13
+ import os
14
+ import anyio
15
+ import threading
16
+ from typing import Dict, Any, Optional, Awaitable
17
+ from contextlib import AsyncExitStack
18
+
19
+ from ..utils.logger import verbose
20
+ from ..types.registry import RegistryServer
21
+ from ..utils.runtime import get_runtime_environment
22
+ from mcp import StdioServerParameters, ClientSession
23
+ from mcp.client.stdio import stdio_client
24
+
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
+ )
32
+
33
+
34
+ async def create_stdio_runner(
35
+ server_details: RegistryServer,
36
+ config: Dict[str, Any],
37
+ api_key: Optional[str] = None,
38
+ analytics_enabled: bool = False
39
+ ) -> Awaitable[None]:
40
+ """创建并运行 STDIO 代理服务器"""
41
+ verbose(f"Starting STDIO proxy runner: {server_details.qualifiedName}")
42
+ is_shutting_down = False
43
+ exit_stack = AsyncExitStack()
44
+
45
+ def handle_error(error: Exception, context: str) -> Exception:
46
+ verbose(f"[Runner] {context}: {error}")
47
+ return error
48
+
49
+ async def cleanup() -> None:
50
+ nonlocal is_shutting_down
51
+ if is_shutting_down:
52
+ verbose("[Runner] Cleanup already in progress, skipping...")
53
+ return
54
+ verbose("[Runner] Starting cleanup...")
55
+ is_shutting_down = True
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
+
90
+ await exit_stack.aclose()
91
+ verbose("[Runner] Resources closed successfully")
92
+ except Exception as error:
93
+ handle_error(error, "Error during cleanup")
94
+ verbose("[Runner] Cleanup completed")
95
+
96
+ async def handle_sigint():
97
+ verbose("[Runner] Received interrupt signal, shutting down...")
98
+ await cleanup()
99
+ # 立即打印一条确认消息,让用户知道CTRL+C已被捕获
100
+ verbose("\n[CTRL+C] 正在关闭服务,请稍候...")
101
+ # 可选:设置一个短暂的超时,然后强制退出
102
+ import threading
103
+ threading.Timer(2.0, lambda: os._exit(0)).start()
104
+
105
+ # 获取连接配置
106
+ stdio_connection = next((conn for conn in server_details.connections if conn.type == "stdio"), None)
107
+ if not stdio_connection:
108
+ raise ValueError("No STDIO connection found")
109
+
110
+ from ..registry import fetch_connection
111
+ formatted_config = config
112
+ verbose(f"Formatted config: {formatted_config}")
113
+ server_config = await fetch_connection(server_details.qualifiedName, formatted_config)
114
+
115
+ if not server_config or not isinstance(server_config, dict):
116
+ raise ValueError("Failed to get valid stdio server configuration")
117
+
118
+ command = server_config.get("command", "python")
119
+ args = server_config.get("args", ["-m", server_details.qualifiedName])
120
+ env_vars = server_config.get("env", {})
121
+ env = get_runtime_environment(env_vars)
122
+
123
+ verbose(f"Using environment: {json.dumps({k: '***' if k.lower().endswith('key') else v for k, v in env.items()})}")
124
+ verbose(f"Executing: {command} {' '.join(args)}")
125
+
126
+ try:
127
+ # 创建服务器进程
128
+ server_params = StdioServerParameters(
129
+ command=command,
130
+ args=args,
131
+ env=env,
132
+ encoding="utf-8"
133
+ )
134
+
135
+ verbose(f"Setting up stdio proxy client for {server_details.qualifiedName}")
136
+ async with stdio_client(server_params, errlog=sys.stderr) as (read_stream, write_stream):
137
+ verbose("Stdio proxy client connection established")
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
+
190
+ # 创建 MCP 客户端会话
191
+ from mcp import ClientSession
192
+ session = await exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
193
+
194
+ # 注册消息处理回调
195
+ def handle_server_message(msg):
196
+ verbose(f"[magenta][server][/magenta] {json.dumps(msg, ensure_ascii=False)}")
197
+ session.on_message = handle_server_message
198
+
199
+ # 初始化 MCP 协议
200
+ if not await initialize_session(session):
201
+ return
202
+
203
+ # 使用简单的同步阻塞循环处理输入和服务器消息
204
+ verbose("[Runner] 开始处理循环,使用同步阻塞模式")
205
+
206
+ # 打印启动消息
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()))
212
+
213
+ # 循环处理客户端请求,直到关闭
214
+ while not is_shutting_down:
215
+ try:
216
+ # 从标准输入读取一行 (同步阻塞)
217
+ line = sys.stdin.readline()
218
+ if not line:
219
+ verbose("[Runner] 标准输入关闭,结束处理")
220
+ break
221
+
222
+ # 处理客户端请求
223
+ message = json.loads(line)
224
+ verbose(f"[stdin] Received message: {line.strip()}")
225
+
226
+ method = message.get("method", "")
227
+ # 根据消息类型处理
228
+ if "id" in message: # 这是请求,需要响应
229
+ response = await process_client_request(message, session)
230
+ sys.stdout.write(response + "\n")
231
+ sys.stdout.flush()
232
+ verbose(f"[stdin] Response sent for method: {method}")
233
+ else: # 这是通知,不需要响应
234
+ # 创建通知对象并发送
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
+ )
242
+ verbose(f"[stdin] Notification sent for method: {method}")
243
+
244
+ verbose(f"[stdin] Processed: {line.strip()}")
245
+
246
+ except json.JSONDecodeError as e:
247
+ error(f"[stdin] JSON decode error: {e}")
248
+ except Exception as e:
249
+ error(f"[stdin] Error processing input: {e}")
250
+ # 如果是请求(有ID),才需要发送错误响应
251
+ try:
252
+ if 'message' in locals() and isinstance(message, dict) and "id" in message:
253
+ error_resp = json.dumps({
254
+ "jsonrpc": "2.0",
255
+ "id": message.get("id"),
256
+ "error": {
257
+ "code": -32700,
258
+ "message": f"Parse error: {str(e)}"
259
+ }
260
+ })
261
+ sys.stdout.write(error_resp + "\n")
262
+ sys.stdout.flush()
263
+ verbose(f"[stdin] Sent error response for parse error")
264
+ except Exception as err:
265
+ error(f"[stdin] Failed to send error response: {err}")
266
+
267
+ # 检查是否有服务器消息需要处理
268
+ # 注意:这部分仍需异步,因为我们需要非阻塞地检查服务器消息
269
+ try:
270
+ # 使用超时机制非阻塞地检查服务器消息
271
+ with anyio.fail_after(0.1): # 设置很短的超时
272
+ message = await read_stream.receive()
273
+ await handle_single_server_message(message)
274
+ except TimeoutError:
275
+ # 超时表示没有消息,继续处理客户端请求
276
+ pass
277
+ except Exception as e:
278
+ error(f"[Runner] 处理服务器消息异常: {e}")
279
+
280
+ verbose("[Runner] 处理循环结束")
281
+
282
+ except Exception as e:
283
+ verbose(f"[red]Error running stdio proxy: {e}[/red]")
284
+ raise
285
+ finally:
286
+ await cleanup()