gfp-mcp 0.1.0__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.
- gfp_mcp-0.1.0.dist-info/METADATA +360 -0
- gfp_mcp-0.1.0.dist-info/RECORD +12 -0
- gfp_mcp-0.1.0.dist-info/WHEEL +5 -0
- gfp_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- gfp_mcp-0.1.0.dist-info/top_level.txt +1 -0
- mcp_standalone/__init__.py +39 -0
- mcp_standalone/client.py +268 -0
- mcp_standalone/config.py +54 -0
- mcp_standalone/mappings.py +286 -0
- mcp_standalone/registry.py +210 -0
- mcp_standalone/server.py +228 -0
- mcp_standalone/tools.py +368 -0
mcp_standalone/config.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Configuration management for MCP standalone server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
__all__ = ["MCPConfig"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MCPConfig:
|
|
12
|
+
"""Configuration for MCP standalone server.
|
|
13
|
+
|
|
14
|
+
Manages environment variables and default settings for the MCP server
|
|
15
|
+
that proxies requests to the FastAPI backend.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# FastAPI base URL (default: http://localhost:8787)
|
|
19
|
+
API_URL: Final[str] = os.getenv("GFP_API_URL", "http://localhost:8787")
|
|
20
|
+
|
|
21
|
+
# Timeout for tool calls in seconds (default: 300 = 5 minutes)
|
|
22
|
+
TIMEOUT: Final[int] = int(os.getenv("GFP_MCP_TIMEOUT", "300"))
|
|
23
|
+
|
|
24
|
+
# Enable debug logging
|
|
25
|
+
DEBUG: Final[bool] = os.getenv("GFP_MCP_DEBUG", "false").lower() in (
|
|
26
|
+
"true",
|
|
27
|
+
"1",
|
|
28
|
+
"yes",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Retry configuration
|
|
32
|
+
MAX_RETRIES: Final[int] = 3
|
|
33
|
+
RETRY_BACKOFF: Final[float] = 0.5 # Initial backoff in seconds
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_api_url(cls, override: str | None = None) -> str:
|
|
37
|
+
"""Get the FastAPI base URL.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
override: Optional URL to override the environment variable
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The API base URL
|
|
44
|
+
"""
|
|
45
|
+
return override or cls.API_URL
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_timeout(cls) -> int:
|
|
49
|
+
"""Get the timeout for tool calls.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Timeout in seconds
|
|
53
|
+
"""
|
|
54
|
+
return cls.TIMEOUT
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Mappings from MCP tools to FastAPI endpoints.
|
|
2
|
+
|
|
3
|
+
This module defines how MCP tool arguments are transformed into HTTP
|
|
4
|
+
requests to the FastAPI backend, and how responses are transformed back.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"TOOL_MAPPINGS",
|
|
14
|
+
"get_mapping",
|
|
15
|
+
"transform_request",
|
|
16
|
+
"transform_response",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EndpointMapping:
|
|
21
|
+
"""Configuration for mapping an MCP tool to a FastAPI endpoint."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
method: str,
|
|
26
|
+
path: str,
|
|
27
|
+
request_transformer: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
|
|
28
|
+
response_transformer: Callable[[Any], Any] | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize endpoint mapping.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
method: HTTP method (GET, POST, etc.)
|
|
34
|
+
path: API endpoint path
|
|
35
|
+
request_transformer: Optional function to transform MCP args to
|
|
36
|
+
HTTP params
|
|
37
|
+
response_transformer: Optional function to transform HTTP response
|
|
38
|
+
to MCP format
|
|
39
|
+
"""
|
|
40
|
+
self.method = method
|
|
41
|
+
self.path = path
|
|
42
|
+
self.request_transformer = request_transformer or (lambda x: x)
|
|
43
|
+
self.response_transformer = response_transformer or (lambda x: x)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_cell_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
"""Transform build_cell MCP args to FastAPI params.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
args: MCP tool arguments
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dict with 'params' key for query parameters
|
|
54
|
+
"""
|
|
55
|
+
return {
|
|
56
|
+
"params": {
|
|
57
|
+
"name": args["name"],
|
|
58
|
+
"with_metadata": args.get("with_metadata", True),
|
|
59
|
+
"register": args.get("register", True),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_cells_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
"""Transform build_cells MCP args to FastAPI params.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
args: MCP tool arguments
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dict with 'params' key for query parameters and 'json_data' for body
|
|
72
|
+
"""
|
|
73
|
+
return {
|
|
74
|
+
"params": {
|
|
75
|
+
"with_metadata": args.get("with_metadata", True),
|
|
76
|
+
"register": args.get("register", True),
|
|
77
|
+
},
|
|
78
|
+
"json_data": args["names"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _list_cells_response(response: Any) -> dict[str, Any]:
|
|
83
|
+
"""Transform list_cells response to MCP format.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
response: FastAPI response (list of cell names)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Formatted response with cell names
|
|
90
|
+
"""
|
|
91
|
+
if isinstance(response, list):
|
|
92
|
+
return {"cells": response, "count": len(response)}
|
|
93
|
+
return response
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_cell_info_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
"""Transform get_cell_info MCP args to FastAPI params.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
args: MCP tool arguments
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict with 'params' key for query parameters
|
|
104
|
+
"""
|
|
105
|
+
return {"params": {"name": args["name"]}}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _download_gds_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
109
|
+
"""Transform download_gds MCP args to FastAPI path.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
args: MCP tool arguments
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict with modified 'path' for the endpoint
|
|
116
|
+
"""
|
|
117
|
+
path = args["path"]
|
|
118
|
+
# The path template in FastAPI is /api/download/{path:path}.gds
|
|
119
|
+
# We need to construct the full path
|
|
120
|
+
return {"path": f"/api/download/{path}.gds"}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _download_gds_response(response: Any) -> dict[str, Any]:
|
|
124
|
+
"""Transform download_gds response to MCP format.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
response: FastAPI response (file path or error)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Formatted response with file path
|
|
131
|
+
"""
|
|
132
|
+
# If response is a string (text), it's likely the file content or path
|
|
133
|
+
if isinstance(response, str):
|
|
134
|
+
return {"status": "success", "message": response}
|
|
135
|
+
return response
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_drc_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
139
|
+
"""Transform check_drc MCP args to FastAPI params.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
args: MCP tool arguments
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dict with 'json_data' key for request body
|
|
146
|
+
"""
|
|
147
|
+
json_data: dict[str, Any] = {"path": args["path"]}
|
|
148
|
+
|
|
149
|
+
# Add optional parameters if provided
|
|
150
|
+
if "pdk" in args and args["pdk"]:
|
|
151
|
+
json_data["pdk"] = args["pdk"]
|
|
152
|
+
if "process" in args and args["process"]:
|
|
153
|
+
json_data["process"] = args["process"]
|
|
154
|
+
if "timeout" in args and args["timeout"]:
|
|
155
|
+
json_data["timeout"] = args["timeout"]
|
|
156
|
+
if "host" in args and args["host"]:
|
|
157
|
+
json_data["host"] = args["host"]
|
|
158
|
+
|
|
159
|
+
return {"json_data": json_data}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _check_connectivity_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
163
|
+
"""Transform check_connectivity MCP args to FastAPI params.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
args: MCP tool arguments
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dict with 'json_data' key for request body
|
|
170
|
+
"""
|
|
171
|
+
return {"json_data": {"path": args["path"]}}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_lvs_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
175
|
+
"""Transform check_lvs MCP args to FastAPI params.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
args: MCP tool arguments
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dict with 'json_data' key for request body
|
|
182
|
+
"""
|
|
183
|
+
return {
|
|
184
|
+
"json_data": {
|
|
185
|
+
"cell": args["cell"],
|
|
186
|
+
"netpath": args["netpath"],
|
|
187
|
+
"cellargs": args.get("cellargs", ""),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Tool name -> Endpoint mapping
|
|
193
|
+
TOOL_MAPPINGS: dict[str, EndpointMapping] = {
|
|
194
|
+
# Phase 1: Core Building Tools
|
|
195
|
+
"build_cell": EndpointMapping(
|
|
196
|
+
method="GET",
|
|
197
|
+
path="/api/build-cell",
|
|
198
|
+
request_transformer=_build_cell_request,
|
|
199
|
+
),
|
|
200
|
+
"build_cells": EndpointMapping(
|
|
201
|
+
method="POST",
|
|
202
|
+
path="/api/build-cells",
|
|
203
|
+
request_transformer=_build_cells_request,
|
|
204
|
+
),
|
|
205
|
+
"list_cells": EndpointMapping(
|
|
206
|
+
method="GET",
|
|
207
|
+
path="/api/cells",
|
|
208
|
+
response_transformer=_list_cells_response,
|
|
209
|
+
),
|
|
210
|
+
"get_cell_info": EndpointMapping(
|
|
211
|
+
method="GET",
|
|
212
|
+
path="/api/cell-info",
|
|
213
|
+
request_transformer=_get_cell_info_request,
|
|
214
|
+
),
|
|
215
|
+
"download_gds": EndpointMapping(
|
|
216
|
+
method="GET",
|
|
217
|
+
path="/api/download/{path}.gds",
|
|
218
|
+
request_transformer=_download_gds_request,
|
|
219
|
+
response_transformer=_download_gds_response,
|
|
220
|
+
),
|
|
221
|
+
# Phase 2: Verification Tools
|
|
222
|
+
"check_drc": EndpointMapping(
|
|
223
|
+
method="POST",
|
|
224
|
+
path="/api/check-drc",
|
|
225
|
+
request_transformer=_check_drc_request,
|
|
226
|
+
),
|
|
227
|
+
"check_connectivity": EndpointMapping(
|
|
228
|
+
method="POST",
|
|
229
|
+
path="/api/check-connectivity",
|
|
230
|
+
request_transformer=_check_connectivity_request,
|
|
231
|
+
),
|
|
232
|
+
"check_lvs": EndpointMapping(
|
|
233
|
+
method="POST",
|
|
234
|
+
path="/api/check-lvs",
|
|
235
|
+
request_transformer=_check_lvs_request,
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_mapping(tool_name: str) -> EndpointMapping | None:
|
|
241
|
+
"""Get the endpoint mapping for a tool.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
tool_name: Name of the MCP tool
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
EndpointMapping or None if not found
|
|
248
|
+
"""
|
|
249
|
+
return TOOL_MAPPINGS.get(tool_name)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def transform_request(
|
|
253
|
+
tool_name: str,
|
|
254
|
+
args: dict[str, Any],
|
|
255
|
+
) -> dict[str, Any]:
|
|
256
|
+
"""Transform MCP tool arguments to HTTP request parameters.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
tool_name: Name of the MCP tool
|
|
260
|
+
args: Tool arguments from MCP
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Dict containing HTTP request parameters (params, json_data, path, etc.)
|
|
264
|
+
"""
|
|
265
|
+
mapping = get_mapping(tool_name)
|
|
266
|
+
if mapping is None:
|
|
267
|
+
return {}
|
|
268
|
+
|
|
269
|
+
return mapping.request_transformer(args)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def transform_response(tool_name: str, response: Any) -> Any:
|
|
273
|
+
"""Transform HTTP response to MCP format.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
tool_name: Name of the MCP tool
|
|
277
|
+
response: HTTP response from FastAPI
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Transformed response for MCP
|
|
281
|
+
"""
|
|
282
|
+
mapping = get_mapping(tool_name)
|
|
283
|
+
if mapping is None:
|
|
284
|
+
return response
|
|
285
|
+
|
|
286
|
+
return mapping.response_transformer(response)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Read-only server registry for MCP that works with gdsfactoryplus registry.
|
|
2
|
+
|
|
3
|
+
This module provides read-only access to the gdsfactoryplus server registry,
|
|
4
|
+
allowing the MCP to discover and connect to running GDSFactory+ server instances.
|
|
5
|
+
The registry file is managed by gdsfactoryplus and located at:
|
|
6
|
+
~/.gdsfactory/server-registry.json
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import psutil
|
|
17
|
+
|
|
18
|
+
HAS_PSUTIL = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
HAS_PSUTIL = False
|
|
21
|
+
|
|
22
|
+
__all__ = ["ServerRegistry", "ServerInfo", "get_registry_path"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_registry_path() -> Path:
|
|
26
|
+
"""Get the path to the server registry file.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Path to ~/.gdsfactory/server-registry.json
|
|
30
|
+
"""
|
|
31
|
+
gdsfactory_dir = Path.home() / ".gdsfactory"
|
|
32
|
+
gdsfactory_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return gdsfactory_dir / "server-registry.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ServerInfo:
|
|
37
|
+
"""Information about a running GDSFactory+ server.
|
|
38
|
+
|
|
39
|
+
This class is compatible with gdsfactoryplus.registry.ServerInfo
|
|
40
|
+
but doesn't require the full gdsfactoryplus package.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
port: int,
|
|
46
|
+
pid: int,
|
|
47
|
+
project_path: str,
|
|
48
|
+
project_name: str,
|
|
49
|
+
pdk: str | None = None,
|
|
50
|
+
started_at: str | None = None,
|
|
51
|
+
last_heartbeat: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize server info.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
port: HTTP server port
|
|
57
|
+
pid: Process ID
|
|
58
|
+
project_path: Absolute path to project directory
|
|
59
|
+
project_name: Human-readable project name
|
|
60
|
+
pdk: PDK name (optional)
|
|
61
|
+
started_at: ISO timestamp when server started
|
|
62
|
+
last_heartbeat: ISO timestamp of last heartbeat
|
|
63
|
+
"""
|
|
64
|
+
self.port = port
|
|
65
|
+
self.pid = pid
|
|
66
|
+
self.project_path = project_path
|
|
67
|
+
self.project_name = project_name
|
|
68
|
+
self.pdk = pdk
|
|
69
|
+
self.started_at = started_at
|
|
70
|
+
self.last_heartbeat = last_heartbeat
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict[str, Any]:
|
|
73
|
+
"""Convert to dictionary."""
|
|
74
|
+
return {
|
|
75
|
+
"port": self.port,
|
|
76
|
+
"pid": self.pid,
|
|
77
|
+
"project_path": self.project_path,
|
|
78
|
+
"project_name": self.project_name,
|
|
79
|
+
"pdk": self.pdk,
|
|
80
|
+
"started_at": self.started_at,
|
|
81
|
+
"last_heartbeat": self.last_heartbeat,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_dict(cls, data: dict[str, Any]) -> ServerInfo:
|
|
86
|
+
"""Create from dictionary."""
|
|
87
|
+
return cls(
|
|
88
|
+
port=data["port"],
|
|
89
|
+
pid=data["pid"],
|
|
90
|
+
project_path=data["project_path"],
|
|
91
|
+
project_name=data["project_name"],
|
|
92
|
+
pdk=data.get("pdk"),
|
|
93
|
+
started_at=data.get("started_at"),
|
|
94
|
+
last_heartbeat=data.get("last_heartbeat"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def is_alive(self) -> bool:
|
|
98
|
+
"""Check if the server process is still running.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if process exists, False otherwise
|
|
102
|
+
If psutil is not available, always returns True
|
|
103
|
+
"""
|
|
104
|
+
if not HAS_PSUTIL:
|
|
105
|
+
# Without psutil, assume the process is alive
|
|
106
|
+
# The registry cleanup is handled by gdsfactoryplus
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
process = psutil.Process(self.pid)
|
|
111
|
+
return process.is_running()
|
|
112
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ServerRegistry:
|
|
117
|
+
"""Read-only registry for discovering active GDSFactory+ servers.
|
|
118
|
+
|
|
119
|
+
This registry reads from the same file as gdsfactoryplus.registry.ServerRegistry
|
|
120
|
+
(~/.gdsfactory/server-registry.json) but provides only read access.
|
|
121
|
+
|
|
122
|
+
The registry file is managed by the GDSFactory+ servers themselves.
|
|
123
|
+
This MCP implementation only reads from it to discover running servers.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, registry_path: Path | None = None) -> None:
|
|
127
|
+
"""Initialize registry with read-only access.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
registry_path: Optional custom path to registry file
|
|
131
|
+
"""
|
|
132
|
+
self.registry_path = registry_path or get_registry_path()
|
|
133
|
+
|
|
134
|
+
def _read_registry(self) -> dict[str, Any]:
|
|
135
|
+
"""Read the registry file.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Registry data as dictionary
|
|
139
|
+
"""
|
|
140
|
+
if not self.registry_path.exists():
|
|
141
|
+
return {"servers": {}}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with self.registry_path.open() as f:
|
|
145
|
+
return json.load(f)
|
|
146
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
147
|
+
return {"servers": {}}
|
|
148
|
+
|
|
149
|
+
def get_server(self, port: int) -> ServerInfo | None:
|
|
150
|
+
"""Get server info by port.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
port: Port number
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
ServerInfo if found and alive, None otherwise
|
|
157
|
+
"""
|
|
158
|
+
data = self._read_registry()
|
|
159
|
+
port_key = str(port)
|
|
160
|
+
|
|
161
|
+
if port_key not in data["servers"]:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
server_info = ServerInfo.from_dict(data["servers"][port_key])
|
|
165
|
+
|
|
166
|
+
# Check if process is still alive (if psutil is available)
|
|
167
|
+
if HAS_PSUTIL and not server_info.is_alive():
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
return server_info
|
|
171
|
+
|
|
172
|
+
def get_server_by_project(self, project: str) -> ServerInfo | None:
|
|
173
|
+
"""Get server info by project name or path.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
project: Project name or path
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Server info if found and alive, None otherwise
|
|
180
|
+
"""
|
|
181
|
+
for server in self.list_servers():
|
|
182
|
+
if project in {
|
|
183
|
+
server.project_name,
|
|
184
|
+
server.project_path,
|
|
185
|
+
Path(server.project_path).name,
|
|
186
|
+
}:
|
|
187
|
+
return server
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def list_servers(self, *, include_dead: bool = False) -> list[ServerInfo]:
|
|
191
|
+
"""List all registered servers.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
include_dead: Include servers with dead processes (default: False)
|
|
195
|
+
Only effective if psutil is available
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of ServerInfo objects
|
|
199
|
+
"""
|
|
200
|
+
data = self._read_registry()
|
|
201
|
+
servers = []
|
|
202
|
+
|
|
203
|
+
for server_data in data["servers"].values():
|
|
204
|
+
server_info = ServerInfo.from_dict(server_data)
|
|
205
|
+
|
|
206
|
+
# Include all servers if psutil is not available or include_dead is True
|
|
207
|
+
if not HAS_PSUTIL or include_dead or server_info.is_alive():
|
|
208
|
+
servers.append(server_info)
|
|
209
|
+
|
|
210
|
+
return servers
|