copex 0.8.4__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.
- copex/__init__.py +69 -0
- copex/checkpoint.py +445 -0
- copex/cli.py +1106 -0
- copex/client.py +725 -0
- copex/config.py +311 -0
- copex/mcp.py +561 -0
- copex/metrics.py +383 -0
- copex/models.py +50 -0
- copex/persistence.py +324 -0
- copex/plan.py +358 -0
- copex/ralph.py +247 -0
- copex/tools.py +404 -0
- copex/ui.py +971 -0
- copex-0.8.4.dist-info/METADATA +511 -0
- copex-0.8.4.dist-info/RECORD +18 -0
- copex-0.8.4.dist-info/WHEEL +4 -0
- copex-0.8.4.dist-info/entry_points.txt +2 -0
- copex-0.8.4.dist-info/licenses/LICENSE +21 -0
copex/mcp.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP (Model Context Protocol) Integration for Copex.
|
|
3
|
+
|
|
4
|
+
Enables:
|
|
5
|
+
- Connecting to external MCP servers
|
|
6
|
+
- Exposing tools from MCP servers to Copex
|
|
7
|
+
- Running a Copex MCP server for other clients
|
|
8
|
+
|
|
9
|
+
Based on the MCP specification for tool/resource sharing.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Awaitable, Callable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MCPTool:
|
|
24
|
+
"""Definition of an MCP tool."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
input_schema: dict[str, Any]
|
|
29
|
+
handler: Callable[..., Awaitable[Any]] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class MCPResource:
|
|
34
|
+
"""Definition of an MCP resource."""
|
|
35
|
+
|
|
36
|
+
uri: str
|
|
37
|
+
name: str
|
|
38
|
+
description: str = ""
|
|
39
|
+
mime_type: str = "text/plain"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class MCPServerConfig:
|
|
44
|
+
"""Configuration for an MCP server connection."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
command: str | list[str] # Command to launch server
|
|
48
|
+
args: list[str] = field(default_factory=list)
|
|
49
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
50
|
+
cwd: str | None = None
|
|
51
|
+
|
|
52
|
+
# Connection settings
|
|
53
|
+
transport: str = "stdio" # "stdio" or "http"
|
|
54
|
+
url: str | None = None # For HTTP transport
|
|
55
|
+
|
|
56
|
+
# Behavior
|
|
57
|
+
auto_start: bool = True
|
|
58
|
+
restart_on_crash: bool = True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MCPTransport(ABC):
|
|
62
|
+
"""Abstract base for MCP transport."""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def connect(self) -> None:
|
|
66
|
+
"""Establish connection."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
async def disconnect(self) -> None:
|
|
71
|
+
"""Close connection."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
async def send(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
"""Send a JSON-RPC message and get response."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
async def receive(self) -> dict[str, Any]:
|
|
81
|
+
"""Receive a message."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StdioTransport(MCPTransport):
|
|
86
|
+
"""MCP transport over stdio (subprocess)."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, config: MCPServerConfig):
|
|
89
|
+
self.config = config
|
|
90
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
91
|
+
self._request_id = 0
|
|
92
|
+
self._pending: dict[int, asyncio.Future] = {}
|
|
93
|
+
self._reader_task: asyncio.Task | None = None
|
|
94
|
+
|
|
95
|
+
async def connect(self) -> None:
|
|
96
|
+
"""Start the MCP server process."""
|
|
97
|
+
cmd = self.config.command
|
|
98
|
+
if isinstance(cmd, str):
|
|
99
|
+
cmd = [cmd] + self.config.args
|
|
100
|
+
else:
|
|
101
|
+
cmd = list(cmd) + self.config.args
|
|
102
|
+
|
|
103
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
104
|
+
*cmd,
|
|
105
|
+
stdin=asyncio.subprocess.PIPE,
|
|
106
|
+
stdout=asyncio.subprocess.PIPE,
|
|
107
|
+
stderr=asyncio.subprocess.PIPE,
|
|
108
|
+
cwd=self.config.cwd,
|
|
109
|
+
env={**dict(__import__("os").environ), **self.config.env} if self.config.env else None,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Start reader task
|
|
113
|
+
self._reader_task = asyncio.create_task(self._reader_loop())
|
|
114
|
+
|
|
115
|
+
# Initialize connection
|
|
116
|
+
await self._initialize()
|
|
117
|
+
|
|
118
|
+
async def _initialize(self) -> None:
|
|
119
|
+
"""Send MCP initialization."""
|
|
120
|
+
await self.send({
|
|
121
|
+
"jsonrpc": "2.0",
|
|
122
|
+
"method": "initialize",
|
|
123
|
+
"params": {
|
|
124
|
+
"protocolVersion": "2024-11-05",
|
|
125
|
+
"capabilities": {
|
|
126
|
+
"tools": {},
|
|
127
|
+
"resources": {},
|
|
128
|
+
},
|
|
129
|
+
"clientInfo": {
|
|
130
|
+
"name": "copex",
|
|
131
|
+
"version": "0.1.0",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# Send initialized notification
|
|
137
|
+
await self._write({
|
|
138
|
+
"jsonrpc": "2.0",
|
|
139
|
+
"method": "notifications/initialized",
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
async def _reader_loop(self) -> None:
|
|
143
|
+
"""Read responses from the server."""
|
|
144
|
+
if not self._process or not self._process.stdout:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
while True:
|
|
148
|
+
try:
|
|
149
|
+
line = await self._process.stdout.readline()
|
|
150
|
+
if not line:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
text = line.decode("utf-8").strip()
|
|
154
|
+
if not text:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
message = json.loads(text)
|
|
159
|
+
except json.JSONDecodeError:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Handle response
|
|
163
|
+
if "id" in message:
|
|
164
|
+
request_id = message["id"]
|
|
165
|
+
if request_id in self._pending:
|
|
166
|
+
future = self._pending.pop(request_id)
|
|
167
|
+
if "error" in message:
|
|
168
|
+
future.set_exception(
|
|
169
|
+
RuntimeError(message["error"].get("message", "Unknown error"))
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
future.set_result(message.get("result"))
|
|
173
|
+
|
|
174
|
+
except asyncio.CancelledError:
|
|
175
|
+
break
|
|
176
|
+
except Exception:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
async def _write(self, message: dict[str, Any]) -> None:
|
|
180
|
+
"""Write a message to the server."""
|
|
181
|
+
if not self._process or not self._process.stdin:
|
|
182
|
+
raise RuntimeError("Not connected")
|
|
183
|
+
|
|
184
|
+
data = json.dumps(message) + "\n"
|
|
185
|
+
self._process.stdin.write(data.encode("utf-8"))
|
|
186
|
+
await self._process.stdin.drain()
|
|
187
|
+
|
|
188
|
+
async def send(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
189
|
+
"""Send a request and wait for response."""
|
|
190
|
+
self._request_id += 1
|
|
191
|
+
request_id = self._request_id
|
|
192
|
+
|
|
193
|
+
message = {**message, "id": request_id}
|
|
194
|
+
|
|
195
|
+
future: asyncio.Future = asyncio.get_event_loop().create_future()
|
|
196
|
+
self._pending[request_id] = future
|
|
197
|
+
|
|
198
|
+
await self._write(message)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
result = await asyncio.wait_for(future, timeout=30.0)
|
|
202
|
+
return result
|
|
203
|
+
except asyncio.TimeoutError:
|
|
204
|
+
self._pending.pop(request_id, None)
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
async def receive(self) -> dict[str, Any]:
|
|
208
|
+
"""Receive is handled by reader loop."""
|
|
209
|
+
raise NotImplementedError("Use send() for request/response")
|
|
210
|
+
|
|
211
|
+
async def disconnect(self) -> None:
|
|
212
|
+
"""Stop the server process."""
|
|
213
|
+
if self._reader_task:
|
|
214
|
+
self._reader_task.cancel()
|
|
215
|
+
try:
|
|
216
|
+
await self._reader_task
|
|
217
|
+
except asyncio.CancelledError:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
if self._process:
|
|
221
|
+
self._process.terminate()
|
|
222
|
+
try:
|
|
223
|
+
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
224
|
+
except asyncio.TimeoutError:
|
|
225
|
+
self._process.kill()
|
|
226
|
+
self._process = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class MCPClient:
|
|
230
|
+
"""
|
|
231
|
+
Client for connecting to MCP servers.
|
|
232
|
+
|
|
233
|
+
Usage:
|
|
234
|
+
config = MCPServerConfig(
|
|
235
|
+
name="my-server",
|
|
236
|
+
command="npx",
|
|
237
|
+
args=["-y", "@my/mcp-server"],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
client = MCPClient(config)
|
|
241
|
+
await client.connect()
|
|
242
|
+
|
|
243
|
+
# List available tools
|
|
244
|
+
tools = await client.list_tools()
|
|
245
|
+
|
|
246
|
+
# Call a tool
|
|
247
|
+
result = await client.call_tool("search", {"query": "hello"})
|
|
248
|
+
|
|
249
|
+
await client.disconnect()
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, config: MCPServerConfig):
|
|
253
|
+
self.config = config
|
|
254
|
+
self._transport: MCPTransport | None = None
|
|
255
|
+
self._tools: list[MCPTool] = []
|
|
256
|
+
self._resources: list[MCPResource] = []
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def connected(self) -> bool:
|
|
260
|
+
"""Check if connected."""
|
|
261
|
+
return self._transport is not None
|
|
262
|
+
|
|
263
|
+
async def connect(self) -> None:
|
|
264
|
+
"""Connect to the MCP server."""
|
|
265
|
+
if self.config.transport == "stdio":
|
|
266
|
+
self._transport = StdioTransport(self.config)
|
|
267
|
+
else:
|
|
268
|
+
raise ValueError(f"Unsupported transport: {self.config.transport}")
|
|
269
|
+
|
|
270
|
+
await self._transport.connect()
|
|
271
|
+
|
|
272
|
+
# Fetch available tools and resources
|
|
273
|
+
await self._refresh_capabilities()
|
|
274
|
+
|
|
275
|
+
async def _refresh_capabilities(self) -> None:
|
|
276
|
+
"""Refresh list of tools and resources."""
|
|
277
|
+
if not self._transport:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# Get tools
|
|
281
|
+
try:
|
|
282
|
+
result = await self._transport.send({
|
|
283
|
+
"jsonrpc": "2.0",
|
|
284
|
+
"method": "tools/list",
|
|
285
|
+
"params": {},
|
|
286
|
+
})
|
|
287
|
+
self._tools = [
|
|
288
|
+
MCPTool(
|
|
289
|
+
name=t["name"],
|
|
290
|
+
description=t.get("description", ""),
|
|
291
|
+
input_schema=t.get("inputSchema", {}),
|
|
292
|
+
)
|
|
293
|
+
for t in result.get("tools", [])
|
|
294
|
+
]
|
|
295
|
+
except Exception:
|
|
296
|
+
self._tools = []
|
|
297
|
+
|
|
298
|
+
# Get resources
|
|
299
|
+
try:
|
|
300
|
+
result = await self._transport.send({
|
|
301
|
+
"jsonrpc": "2.0",
|
|
302
|
+
"method": "resources/list",
|
|
303
|
+
"params": {},
|
|
304
|
+
})
|
|
305
|
+
self._resources = [
|
|
306
|
+
MCPResource(
|
|
307
|
+
uri=r["uri"],
|
|
308
|
+
name=r["name"],
|
|
309
|
+
description=r.get("description", ""),
|
|
310
|
+
mime_type=r.get("mimeType", "text/plain"),
|
|
311
|
+
)
|
|
312
|
+
for r in result.get("resources", [])
|
|
313
|
+
]
|
|
314
|
+
except Exception:
|
|
315
|
+
self._resources = []
|
|
316
|
+
|
|
317
|
+
async def disconnect(self) -> None:
|
|
318
|
+
"""Disconnect from the MCP server."""
|
|
319
|
+
if self._transport:
|
|
320
|
+
await self._transport.disconnect()
|
|
321
|
+
self._transport = None
|
|
322
|
+
|
|
323
|
+
async def list_tools(self) -> list[MCPTool]:
|
|
324
|
+
"""Get list of available tools."""
|
|
325
|
+
return self._tools.copy()
|
|
326
|
+
|
|
327
|
+
async def list_resources(self) -> list[MCPResource]:
|
|
328
|
+
"""Get list of available resources."""
|
|
329
|
+
return self._resources.copy()
|
|
330
|
+
|
|
331
|
+
async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
|
|
332
|
+
"""
|
|
333
|
+
Call a tool on the MCP server.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: Tool name
|
|
337
|
+
arguments: Tool arguments
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Tool result
|
|
341
|
+
"""
|
|
342
|
+
if not self._transport:
|
|
343
|
+
raise RuntimeError("Not connected")
|
|
344
|
+
|
|
345
|
+
result = await self._transport.send({
|
|
346
|
+
"jsonrpc": "2.0",
|
|
347
|
+
"method": "tools/call",
|
|
348
|
+
"params": {
|
|
349
|
+
"name": name,
|
|
350
|
+
"arguments": arguments,
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
# Extract content from result
|
|
355
|
+
content = result.get("content", [])
|
|
356
|
+
if content and len(content) == 1:
|
|
357
|
+
return content[0].get("text", content[0])
|
|
358
|
+
return content
|
|
359
|
+
|
|
360
|
+
async def read_resource(self, uri: str) -> str:
|
|
361
|
+
"""
|
|
362
|
+
Read a resource from the MCP server.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
uri: Resource URI
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Resource content
|
|
369
|
+
"""
|
|
370
|
+
if not self._transport:
|
|
371
|
+
raise RuntimeError("Not connected")
|
|
372
|
+
|
|
373
|
+
result = await self._transport.send({
|
|
374
|
+
"jsonrpc": "2.0",
|
|
375
|
+
"method": "resources/read",
|
|
376
|
+
"params": {"uri": uri},
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
contents = result.get("contents", [])
|
|
380
|
+
if contents:
|
|
381
|
+
return contents[0].get("text", "")
|
|
382
|
+
return ""
|
|
383
|
+
|
|
384
|
+
def get_copex_tools(self) -> list[dict[str, Any]]:
|
|
385
|
+
"""
|
|
386
|
+
Get tools formatted for Copex session.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of tool definitions for create_session
|
|
390
|
+
"""
|
|
391
|
+
return [
|
|
392
|
+
{
|
|
393
|
+
"name": tool.name,
|
|
394
|
+
"description": tool.description,
|
|
395
|
+
"parameters": tool.input_schema,
|
|
396
|
+
}
|
|
397
|
+
for tool in self._tools
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class MCPManager:
|
|
402
|
+
"""
|
|
403
|
+
Manages multiple MCP server connections.
|
|
404
|
+
|
|
405
|
+
Usage:
|
|
406
|
+
manager = MCPManager()
|
|
407
|
+
|
|
408
|
+
# Add servers
|
|
409
|
+
manager.add_server(MCPServerConfig(
|
|
410
|
+
name="github",
|
|
411
|
+
command="npx",
|
|
412
|
+
args=["-y", "@github/mcp-server"],
|
|
413
|
+
))
|
|
414
|
+
|
|
415
|
+
# Connect all
|
|
416
|
+
await manager.connect_all()
|
|
417
|
+
|
|
418
|
+
# Get all tools across servers
|
|
419
|
+
all_tools = manager.get_all_tools()
|
|
420
|
+
|
|
421
|
+
# Call a tool (auto-routes to correct server)
|
|
422
|
+
result = await manager.call_tool("github:search_repos", {...})
|
|
423
|
+
|
|
424
|
+
await manager.disconnect_all()
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def __init__(self):
|
|
428
|
+
self._servers: dict[str, MCPServerConfig] = {}
|
|
429
|
+
self._clients: dict[str, MCPClient] = {}
|
|
430
|
+
|
|
431
|
+
def add_server(self, config: MCPServerConfig) -> None:
|
|
432
|
+
"""Add an MCP server configuration."""
|
|
433
|
+
self._servers[config.name] = config
|
|
434
|
+
|
|
435
|
+
def remove_server(self, name: str) -> None:
|
|
436
|
+
"""Remove an MCP server."""
|
|
437
|
+
self._servers.pop(name, None)
|
|
438
|
+
|
|
439
|
+
async def connect(self, name: str) -> MCPClient:
|
|
440
|
+
"""Connect to a specific server."""
|
|
441
|
+
config = self._servers.get(name)
|
|
442
|
+
if not config:
|
|
443
|
+
raise ValueError(f"Unknown server: {name}")
|
|
444
|
+
|
|
445
|
+
client = MCPClient(config)
|
|
446
|
+
await client.connect()
|
|
447
|
+
self._clients[name] = client
|
|
448
|
+
return client
|
|
449
|
+
|
|
450
|
+
async def connect_all(self) -> None:
|
|
451
|
+
"""Connect to all configured servers."""
|
|
452
|
+
for name in self._servers:
|
|
453
|
+
if name not in self._clients:
|
|
454
|
+
await self.connect(name)
|
|
455
|
+
|
|
456
|
+
async def disconnect(self, name: str) -> None:
|
|
457
|
+
"""Disconnect from a specific server."""
|
|
458
|
+
client = self._clients.pop(name, None)
|
|
459
|
+
if client:
|
|
460
|
+
await client.disconnect()
|
|
461
|
+
|
|
462
|
+
async def disconnect_all(self) -> None:
|
|
463
|
+
"""Disconnect from all servers."""
|
|
464
|
+
for name in list(self._clients.keys()):
|
|
465
|
+
await self.disconnect(name)
|
|
466
|
+
|
|
467
|
+
def get_client(self, name: str) -> MCPClient | None:
|
|
468
|
+
"""Get a connected client by name."""
|
|
469
|
+
return self._clients.get(name)
|
|
470
|
+
|
|
471
|
+
def get_all_tools(self) -> list[dict[str, Any]]:
|
|
472
|
+
"""Get all tools across all connected servers."""
|
|
473
|
+
tools = []
|
|
474
|
+
for server_name, client in self._clients.items():
|
|
475
|
+
for tool in client._tools:
|
|
476
|
+
tools.append({
|
|
477
|
+
"name": f"{server_name}:{tool.name}",
|
|
478
|
+
"description": f"[{server_name}] {tool.description}",
|
|
479
|
+
"parameters": tool.input_schema,
|
|
480
|
+
"_server": server_name,
|
|
481
|
+
"_tool": tool.name,
|
|
482
|
+
})
|
|
483
|
+
return tools
|
|
484
|
+
|
|
485
|
+
async def call_tool(self, qualified_name: str, arguments: dict[str, Any]) -> Any:
|
|
486
|
+
"""
|
|
487
|
+
Call a tool by qualified name (server:tool).
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
qualified_name: "server_name:tool_name"
|
|
491
|
+
arguments: Tool arguments
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Tool result
|
|
495
|
+
"""
|
|
496
|
+
if ":" not in qualified_name:
|
|
497
|
+
raise ValueError(f"Expected qualified name 'server:tool', got: {qualified_name}")
|
|
498
|
+
|
|
499
|
+
server_name, tool_name = qualified_name.split(":", 1)
|
|
500
|
+
client = self._clients.get(server_name)
|
|
501
|
+
|
|
502
|
+
if not client:
|
|
503
|
+
raise ValueError(f"Server not connected: {server_name}")
|
|
504
|
+
|
|
505
|
+
return await client.call_tool(tool_name, arguments)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def load_mcp_config(path: Path | str | None = None) -> list[MCPServerConfig]:
|
|
509
|
+
"""
|
|
510
|
+
Load MCP server configurations from file.
|
|
511
|
+
|
|
512
|
+
Default locations:
|
|
513
|
+
- .copex/mcp.json (project)
|
|
514
|
+
- ~/.copex/mcp.json (global)
|
|
515
|
+
|
|
516
|
+
Config format:
|
|
517
|
+
{
|
|
518
|
+
"servers": {
|
|
519
|
+
"github": {
|
|
520
|
+
"command": "npx",
|
|
521
|
+
"args": ["-y", "@github/mcp-server"],
|
|
522
|
+
"env": {"GITHUB_TOKEN": "..."}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
"""
|
|
527
|
+
if path:
|
|
528
|
+
config_path = Path(path)
|
|
529
|
+
else:
|
|
530
|
+
# Try project, then global
|
|
531
|
+
project_path = Path(".copex/mcp.json")
|
|
532
|
+
global_path = Path.home() / ".copex" / "mcp.json"
|
|
533
|
+
|
|
534
|
+
if project_path.exists():
|
|
535
|
+
config_path = project_path
|
|
536
|
+
elif global_path.exists():
|
|
537
|
+
config_path = global_path
|
|
538
|
+
else:
|
|
539
|
+
return []
|
|
540
|
+
|
|
541
|
+
if not config_path.exists():
|
|
542
|
+
return []
|
|
543
|
+
|
|
544
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
545
|
+
data = json.load(f)
|
|
546
|
+
|
|
547
|
+
servers = []
|
|
548
|
+
for name, config in data.get("servers", {}).items():
|
|
549
|
+
servers.append(MCPServerConfig(
|
|
550
|
+
name=name,
|
|
551
|
+
command=config["command"],
|
|
552
|
+
args=config.get("args", []),
|
|
553
|
+
env=config.get("env", {}),
|
|
554
|
+
cwd=config.get("cwd"),
|
|
555
|
+
transport=config.get("transport", "stdio"),
|
|
556
|
+
url=config.get("url"),
|
|
557
|
+
auto_start=config.get("auto_start", True),
|
|
558
|
+
restart_on_crash=config.get("restart_on_crash", True),
|
|
559
|
+
))
|
|
560
|
+
|
|
561
|
+
return servers
|