forked from themanojdesai/python-a2a
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver_config.py
More file actions
334 lines (265 loc) · 9.71 KB
/
Copy pathserver_config.py
File metadata and controls
334 lines (265 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
"""
MCP Server Configuration System.
This module provides a configuration system for MCP servers that matches
how Claude Desktop and other MCP hosts configure and run servers.
Key Concepts:
- MCP servers are ALWAYS run as subprocesses (not remote HTTP services)
- Communication happens via stdio (stdin/stdout) using JSON-RPC
- Servers can be Docker containers, NPX packages, or direct executables
- Configuration follows Claude Desktop's JSON format
"""
import asyncio
import json
import logging
import os
import subprocess
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Union
from pathlib import Path
from .clients import MCPClient, create_stdio_client
logger = logging.getLogger(__name__)
@dataclass
class ServerConfig:
"""
Configuration for an MCP server.
This matches Claude Desktop's configuration format:
{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"],
"env": {"KEY": "value"}
}
"""
command: str
args: List[str] = field(default_factory=list)
env: Dict[str, str] = field(default_factory=dict)
package_name: Optional[str] = None # For better error messages
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ServerConfig":
"""Create from dictionary configuration."""
return cls(
command=data["command"],
args=data.get("args", []),
env=data.get("env", {}),
package_name=data.get("package_name")
)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"command": self.command,
"args": self.args,
"env": self.env
}
class MCPServerRunner:
"""
Runs MCP servers as subprocesses and manages their lifecycle.
This class handles:
- Starting server processes
- Creating stdio clients for communication
- Managing server lifecycle
- Handling errors and restarts
"""
def __init__(self, name: str, config: ServerConfig):
"""
Initialize server runner.
Args:
name: Server name (e.g. "github", "filesystem")
config: Server configuration
"""
self.name = name
self.config = config
self.process: Optional[subprocess.Popen] = None
self.client: Optional[MCPClient] = None
self._running = False
async def start(self) -> MCPClient:
"""
Start the server process and create a client.
Returns:
MCPClient connected to the server
Raises:
RuntimeError: If server fails to start
"""
if self._running:
raise RuntimeError(f"Server {self.name} is already running")
try:
# Prepare environment
env = os.environ.copy()
env.update(self.config.env)
# Start the process and create client
logger.info(f"Starting MCP server {self.name}: {self.config.command} {' '.join(self.config.args)}")
# Create stdio client - this will start the process internally
self.client = await create_stdio_client(
command=[self.config.command] + self.config.args,
env=env
)
# The process is now managed by the client
self.process = None
self._running = True
logger.info(f"MCP server {self.name} started successfully")
return self.client
except Exception as e:
logger.error(f"Failed to start MCP server {self.name}: {e}")
await self.stop()
raise RuntimeError(f"Failed to start server {self.name}: {e}")
async def stop(self):
"""Stop the server process."""
if self.client:
try:
await self.client.disconnect()
except Exception as e:
logger.warning(f"Error disconnecting client for {self.name}: {e}")
self.client = None
self._running = False
logger.info(f"MCP server {self.name} stopped")
@property
def is_running(self) -> bool:
"""Check if server is running."""
return self._running and self.client is not None and self.client.is_connected
class MCPServerManager:
"""
Manages multiple MCP servers.
This class provides:
- Loading server configurations
- Starting/stopping servers
- Accessing server clients
- Health monitoring
"""
def __init__(self):
"""Initialize the server manager."""
self.servers: Dict[str, MCPServerRunner] = {}
self._configs: Dict[str, ServerConfig] = {}
def add_server(self, name: str, config: Union[ServerConfig, Dict[str, Any]]):
"""
Add a server configuration.
Args:
name: Server name
config: Server configuration (ServerConfig or dict)
"""
if isinstance(config, dict):
config = ServerConfig.from_dict(config)
self._configs[name] = config
logger.info(f"Added server configuration: {name}")
def load_config(self, config_path: Union[str, Path]):
"""
Load server configurations from a JSON file.
The file should have the same format as Claude Desktop:
{
"mcpServers": {
"github": {
"command": "docker",
"args": [...],
"env": {...}
},
"filesystem": {
"command": "npx",
"args": [...]
}
}
}
Args:
config_path: Path to configuration file
"""
config_path = Path(config_path)
with open(config_path, 'r') as f:
data = json.load(f)
servers = data.get("mcpServers", {})
for name, config in servers.items():
self.add_server(name, config)
logger.info(f"Loaded {len(servers)} server configurations from {config_path}")
async def start_server(self, name: str) -> MCPClient:
"""
Start a specific server.
Args:
name: Server name
Returns:
MCPClient connected to the server
Raises:
KeyError: If server not configured
RuntimeError: If server fails to start
"""
if name not in self._configs:
raise KeyError(f"Server {name} not configured")
if name in self.servers and self.servers[name].is_running:
logger.info(f"Server {name} is already running")
return self.servers[name].client
runner = MCPServerRunner(name, self._configs[name])
client = await runner.start()
self.servers[name] = runner
return client
async def stop_server(self, name: str):
"""
Stop a specific server.
Args:
name: Server name
"""
if name in self.servers:
await self.servers[name].stop()
del self.servers[name]
async def start_all(self) -> Dict[str, MCPClient]:
"""
Start all configured servers.
Returns:
Dictionary mapping server names to clients
"""
clients = {}
for name in self._configs:
try:
client = await self.start_server(name)
clients[name] = client
except Exception as e:
logger.error(f"Failed to start server {name}: {e}")
return clients
async def stop_all(self):
"""Stop all running servers."""
server_names = list(self.servers.keys())
for name in server_names:
try:
await self.stop_server(name)
except Exception as e:
logger.error(f"Error stopping server {name}: {e}")
def get_client(self, name: str) -> Optional[MCPClient]:
"""
Get client for a running server.
Args:
name: Server name
Returns:
MCPClient if server is running, None otherwise
"""
if name in self.servers and self.servers[name].is_running:
return self.servers[name].client
return None
def list_configured(self) -> List[str]:
"""List all configured server names."""
return list(self._configs.keys())
def list_running(self) -> List[str]:
"""List all running server names."""
return [name for name, runner in self.servers.items() if runner.is_running]
# Convenience functions
def create_github_config(token: str) -> ServerConfig:
"""
Create configuration for GitHub MCP server.
Args:
token: GitHub personal access token
Returns:
ServerConfig for GitHub MCP server
"""
return ServerConfig(
command="docker",
args=[
"run", "-i", "--rm",
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
env={"GITHUB_PERSONAL_ACCESS_TOKEN": token}
)
def create_filesystem_config(allowed_paths: List[str]) -> ServerConfig:
"""
Create configuration for filesystem MCP server.
Args:
allowed_paths: List of allowed directory paths
Returns:
ServerConfig for filesystem MCP server
"""
return ServerConfig(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem"] + allowed_paths
)