gfp-mcp 0.1.0__py3-none-any.whl → 0.2.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.
- gfp_mcp-0.2.4.dist-info/METADATA +227 -0
- gfp_mcp-0.2.4.dist-info/RECORD +14 -0
- {gfp_mcp-0.1.0.dist-info → gfp_mcp-0.2.4.dist-info}/WHEEL +1 -1
- gfp_mcp-0.2.4.dist-info/licenses/LICENSE +4 -0
- mcp_standalone/__init__.py +4 -1
- mcp_standalone/client.py +61 -19
- mcp_standalone/config.py +3 -7
- mcp_standalone/mappings.py +341 -62
- mcp_standalone/registry.py +0 -4
- mcp_standalone/resources.py +138 -0
- mcp_standalone/server.py +78 -24
- mcp_standalone/tools.py +169 -71
- gfp_mcp-0.1.0.dist-info/METADATA +0 -360
- gfp_mcp-0.1.0.dist-info/RECORD +0 -12
- {gfp_mcp-0.1.0.dist-info → gfp_mcp-0.2.4.dist-info}/entry_points.txt +0 -0
- {gfp_mcp-0.1.0.dist-info → gfp_mcp-0.2.4.dist-info}/top_level.txt +0 -0
mcp_standalone/mappings.py
CHANGED
|
@@ -6,6 +6,7 @@ requests to the FastAPI backend, and how responses are transformed back.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
@@ -43,24 +44,6 @@ class EndpointMapping:
|
|
|
43
44
|
self.response_transformer = response_transformer or (lambda x: x)
|
|
44
45
|
|
|
45
46
|
|
|
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
47
|
def _build_cells_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
65
48
|
"""Transform build_cells MCP args to FastAPI params.
|
|
66
49
|
|
|
@@ -105,58 +88,272 @@ def _get_cell_info_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
|
105
88
|
return {"params": {"name": args["name"]}}
|
|
106
89
|
|
|
107
90
|
|
|
108
|
-
def
|
|
109
|
-
"""Transform
|
|
91
|
+
def _check_drc_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
92
|
+
"""Transform check_drc MCP args to FastAPI params.
|
|
110
93
|
|
|
111
94
|
Args:
|
|
112
95
|
args: MCP tool arguments
|
|
113
96
|
|
|
114
97
|
Returns:
|
|
115
|
-
Dict with
|
|
98
|
+
Dict with 'json_data' key for request body
|
|
116
99
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
json_data: dict[str, Any] = {"path": args["path"]}
|
|
101
|
+
|
|
102
|
+
if "pdk" in args and args["pdk"]:
|
|
103
|
+
json_data["pdk"] = args["pdk"]
|
|
104
|
+
if "process" in args and args["process"]:
|
|
105
|
+
json_data["process"] = args["process"]
|
|
106
|
+
if "timeout" in args and args["timeout"]:
|
|
107
|
+
json_data["timeout"] = args["timeout"]
|
|
108
|
+
if "host" in args and args["host"]:
|
|
109
|
+
json_data["host"] = args["host"]
|
|
110
|
+
|
|
111
|
+
return {"json_data": json_data}
|
|
121
112
|
|
|
122
113
|
|
|
123
|
-
def
|
|
124
|
-
"""Transform
|
|
114
|
+
def _check_drc_response(response: Any) -> dict[str, Any]:
|
|
115
|
+
"""Transform check_drc response to LLM-friendly format.
|
|
116
|
+
|
|
117
|
+
Parses KLayout RDB XML and extracts actionable violation summary
|
|
118
|
+
without polygon coordinates or verbose metadata. Computes simplified
|
|
119
|
+
location data (bounding box, centroid, size) from polygon coordinates
|
|
120
|
+
to reduce token usage by ~82% while preserving all violation details.
|
|
125
121
|
|
|
126
122
|
Args:
|
|
127
|
-
response: FastAPI response (
|
|
123
|
+
response: FastAPI response (XML string or dict with 'content' key)
|
|
128
124
|
|
|
129
125
|
Returns:
|
|
130
|
-
|
|
126
|
+
Structured summary with all violations including location data,
|
|
127
|
+
or error dict if parsing fails
|
|
131
128
|
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
try:
|
|
130
|
+
if isinstance(response, dict) and "content" in response:
|
|
131
|
+
xml_str = response["content"]
|
|
132
|
+
elif isinstance(response, str):
|
|
133
|
+
xml_str = response
|
|
134
|
+
else:
|
|
135
|
+
return {
|
|
136
|
+
"error": f"Unexpected response type: {type(response).__name__}",
|
|
137
|
+
"suggestion": "Expected XML string or dict with 'content' key",
|
|
138
|
+
}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return {
|
|
141
|
+
"error": f"Failed to extract XML from response: {e}",
|
|
142
|
+
"response_preview": str(response)[:500],
|
|
143
|
+
}
|
|
136
144
|
|
|
145
|
+
try:
|
|
146
|
+
root = ET.fromstring(xml_str)
|
|
147
|
+
except ET.ParseError as e:
|
|
148
|
+
return {
|
|
149
|
+
"error": f"Failed to parse DRC XML: {e}",
|
|
150
|
+
"raw_preview": xml_str[:500],
|
|
151
|
+
"suggestion": "Check if DRC server returned valid XML",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
categories_map = {}
|
|
155
|
+
for category in root.findall(".//categories/category"):
|
|
156
|
+
name_elem = category.find("name")
|
|
157
|
+
desc_elem = category.find("description")
|
|
158
|
+
if name_elem is not None and name_elem.text:
|
|
159
|
+
categories_map[name_elem.text] = (
|
|
160
|
+
desc_elem.text
|
|
161
|
+
if desc_elem is not None and desc_elem.text
|
|
162
|
+
else name_elem.text
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
cells = []
|
|
166
|
+
for cell in root.findall(".//cells/cell"):
|
|
167
|
+
name_elem = cell.find("name")
|
|
168
|
+
if name_elem is not None and name_elem.text:
|
|
169
|
+
cells.append(name_elem.text)
|
|
170
|
+
|
|
171
|
+
violations = []
|
|
172
|
+
violation_counts: dict[str, int] = {}
|
|
173
|
+
|
|
174
|
+
for item in root.findall(".//items/item"):
|
|
175
|
+
category_elem = item.find("category")
|
|
176
|
+
cell_elem = item.find("cell")
|
|
177
|
+
comment_elem = item.find("comment")
|
|
178
|
+
values_elem = item.find("values")
|
|
179
|
+
|
|
180
|
+
if category_elem is None or category_elem.text is None:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
category = category_elem.text
|
|
184
|
+
cell = cell_elem.text if cell_elem is not None and cell_elem.text else "unknown"
|
|
185
|
+
description = (
|
|
186
|
+
comment_elem.text
|
|
187
|
+
if comment_elem is not None and comment_elem.text
|
|
188
|
+
else categories_map.get(category, category)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
location = _parse_polygon_location(values_elem)
|
|
192
|
+
|
|
193
|
+
violation = {
|
|
194
|
+
"category": category,
|
|
195
|
+
"cell": cell,
|
|
196
|
+
"description": description,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if location is not None:
|
|
200
|
+
violation["location"] = location
|
|
201
|
+
elif values_elem is not None:
|
|
202
|
+
violation["location_warning"] = "Could not parse coordinates"
|
|
203
|
+
|
|
204
|
+
violations.append(violation)
|
|
205
|
+
|
|
206
|
+
violation_counts[category] = violation_counts.get(category, 0) + 1
|
|
207
|
+
|
|
208
|
+
total_violations = len(violations)
|
|
209
|
+
status = "PASSED" if total_violations == 0 else "FAILED"
|
|
210
|
+
|
|
211
|
+
summary = {
|
|
212
|
+
"total_violations": total_violations,
|
|
213
|
+
"total_categories": len(violation_counts),
|
|
214
|
+
"cells_checked": cells,
|
|
215
|
+
"status": status,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if total_violations == 0:
|
|
219
|
+
summary["message"] = "No DRC violations found"
|
|
220
|
+
|
|
221
|
+
violations_by_category = [
|
|
222
|
+
{
|
|
223
|
+
"category": cat,
|
|
224
|
+
"description": categories_map.get(cat, cat),
|
|
225
|
+
"count": count,
|
|
226
|
+
}
|
|
227
|
+
for cat, count in sorted(
|
|
228
|
+
violation_counts.items(),
|
|
229
|
+
key=lambda x: x[1],
|
|
230
|
+
reverse=True,
|
|
231
|
+
)
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
recommendations = _generate_drc_recommendations(
|
|
235
|
+
total_violations,
|
|
236
|
+
violations_by_category,
|
|
237
|
+
cells,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"summary": summary,
|
|
242
|
+
"violations_by_category": violations_by_category,
|
|
243
|
+
"violations": violations,
|
|
244
|
+
"recommendations": recommendations,
|
|
245
|
+
}
|
|
137
246
|
|
|
138
|
-
|
|
139
|
-
|
|
247
|
+
|
|
248
|
+
def _parse_polygon_location(values_elem: ET.Element | None) -> dict[str, Any] | None:
|
|
249
|
+
"""Parse polygon coordinates and compute location data.
|
|
140
250
|
|
|
141
251
|
Args:
|
|
142
|
-
|
|
252
|
+
values_elem: XML element containing polygon coordinates
|
|
143
253
|
|
|
144
254
|
Returns:
|
|
145
|
-
Dict with
|
|
255
|
+
Dict with bbox, centroid, and size, or None if parsing fails
|
|
146
256
|
"""
|
|
147
|
-
|
|
257
|
+
if values_elem is None:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
value_elem = values_elem.find("value")
|
|
261
|
+
if value_elem is None or value_elem.text is None:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
value_text = value_elem.text
|
|
265
|
+
if "polygon:" not in value_text:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
coords_str = value_text.replace("polygon:", "").strip().strip("()")
|
|
270
|
+
if not coords_str:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
pairs = coords_str.split(";")
|
|
274
|
+
coords = []
|
|
275
|
+
for pair in pairs:
|
|
276
|
+
if "," not in pair:
|
|
277
|
+
continue
|
|
278
|
+
x_str, y_str = pair.split(",", 1)
|
|
279
|
+
coords.append((float(x_str), float(y_str)))
|
|
280
|
+
|
|
281
|
+
if not coords:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
xs = [x for x, y in coords]
|
|
285
|
+
ys = [y for x, y in coords]
|
|
286
|
+
|
|
287
|
+
bbox = {
|
|
288
|
+
"min_x": round(min(xs), 3),
|
|
289
|
+
"min_y": round(min(ys), 3),
|
|
290
|
+
"max_x": round(max(xs), 3),
|
|
291
|
+
"max_y": round(max(ys), 3),
|
|
292
|
+
}
|
|
148
293
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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"]
|
|
294
|
+
centroid = {
|
|
295
|
+
"x": round(sum(xs) / len(xs), 3),
|
|
296
|
+
"y": round(sum(ys) / len(ys), 3),
|
|
297
|
+
}
|
|
158
298
|
|
|
159
|
-
|
|
299
|
+
size = {
|
|
300
|
+
"width": round(bbox["max_x"] - bbox["min_x"], 3),
|
|
301
|
+
"height": round(bbox["max_y"] - bbox["min_y"], 3),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"bbox": bbox,
|
|
306
|
+
"centroid": centroid,
|
|
307
|
+
"size": size,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
except (ValueError, IndexError):
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _generate_drc_recommendations(
|
|
315
|
+
total_violations: int,
|
|
316
|
+
violations_by_category: list[dict[str, Any]],
|
|
317
|
+
cells: list[str],
|
|
318
|
+
) -> list[str]:
|
|
319
|
+
"""Generate actionable recommendations based on DRC results.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
total_violations: Total number of violations
|
|
323
|
+
violations_by_category: List of violation categories with counts
|
|
324
|
+
cells: List of cells checked
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List of recommendation strings
|
|
328
|
+
"""
|
|
329
|
+
if total_violations == 0:
|
|
330
|
+
return ["Design passes all DRC checks"]
|
|
331
|
+
|
|
332
|
+
recommendations = []
|
|
333
|
+
|
|
334
|
+
if violations_by_category:
|
|
335
|
+
top_category = violations_by_category[0]
|
|
336
|
+
if top_category["count"] > 10:
|
|
337
|
+
recommendations.append(
|
|
338
|
+
f"Focus on {top_category['category']} violations "
|
|
339
|
+
f"({top_category['count']:,} occurrences)"
|
|
340
|
+
)
|
|
341
|
+
elif top_category["count"] > 1:
|
|
342
|
+
recommendations.append(
|
|
343
|
+
f"Check {top_category['category']} rules in {', '.join(cells)}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if len(violations_by_category) > 1:
|
|
347
|
+
recommendations.append(
|
|
348
|
+
f"Review {len(violations_by_category)} different DRC rule categories"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if total_violations > 100:
|
|
352
|
+
recommendations.append(
|
|
353
|
+
"Consider fixing systematic issues first to reduce violation count"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return recommendations
|
|
160
357
|
|
|
161
358
|
|
|
162
359
|
def _check_connectivity_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -189,14 +386,78 @@ def _check_lvs_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
|
189
386
|
}
|
|
190
387
|
|
|
191
388
|
|
|
192
|
-
|
|
389
|
+
def _simulate_component_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
390
|
+
"""Transform simulate_component MCP args to FastAPI params.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
args: MCP tool arguments
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Dict with 'params' key for query parameters
|
|
397
|
+
"""
|
|
398
|
+
return {"params": {"name": args["name"]}}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _get_port_center_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
402
|
+
"""Transform get_port_center MCP args to FastAPI params.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
args: MCP tool arguments
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Dict with 'params' key for query parameters
|
|
409
|
+
"""
|
|
410
|
+
return {
|
|
411
|
+
"params": {
|
|
412
|
+
"netlist": args["netlist"],
|
|
413
|
+
"instance": args["instance"],
|
|
414
|
+
"port": args["port"],
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _generate_bbox_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
420
|
+
"""Transform generate_bbox MCP args to FastAPI params.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
args: MCP tool arguments
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dict with 'json_data' key for request body
|
|
427
|
+
"""
|
|
428
|
+
json_data: dict[str, Any] = {"path": args["path"]}
|
|
429
|
+
|
|
430
|
+
if "outpath" in args and args["outpath"]:
|
|
431
|
+
json_data["outpath"] = args["outpath"]
|
|
432
|
+
if "layers_to_keep" in args and args["layers_to_keep"]:
|
|
433
|
+
json_data["layers_to_keep"] = args["layers_to_keep"]
|
|
434
|
+
if "bbox_layer" in args and args["bbox_layer"]:
|
|
435
|
+
json_data["bbox_layer"] = args["bbox_layer"]
|
|
436
|
+
if "ignore_ports" in args:
|
|
437
|
+
json_data["ignore_ports"] = args["ignore_ports"]
|
|
438
|
+
|
|
439
|
+
return {"json_data": json_data}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _freeze_cell_request(args: dict[str, Any]) -> dict[str, Any]:
|
|
443
|
+
"""Transform freeze_cell MCP args to FastAPI params.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
args: MCP tool arguments
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Dict with 'path' and 'json_data' for the request
|
|
450
|
+
"""
|
|
451
|
+
cell_name = args["cell_name"]
|
|
452
|
+
kwargs = args.get("kwargs", {})
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"path": f"/freeze/{cell_name}",
|
|
456
|
+
"json_data": kwargs, # httpx will JSON-encode this to a string
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
193
460
|
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
461
|
"build_cells": EndpointMapping(
|
|
201
462
|
method="POST",
|
|
202
463
|
path="/api/build-cells",
|
|
@@ -212,17 +473,11 @@ TOOL_MAPPINGS: dict[str, EndpointMapping] = {
|
|
|
212
473
|
path="/api/cell-info",
|
|
213
474
|
request_transformer=_get_cell_info_request,
|
|
214
475
|
),
|
|
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
476
|
"check_drc": EndpointMapping(
|
|
223
477
|
method="POST",
|
|
224
478
|
path="/api/check-drc",
|
|
225
479
|
request_transformer=_check_drc_request,
|
|
480
|
+
response_transformer=_check_drc_response,
|
|
226
481
|
),
|
|
227
482
|
"check_connectivity": EndpointMapping(
|
|
228
483
|
method="POST",
|
|
@@ -234,6 +489,30 @@ TOOL_MAPPINGS: dict[str, EndpointMapping] = {
|
|
|
234
489
|
path="/api/check-lvs",
|
|
235
490
|
request_transformer=_check_lvs_request,
|
|
236
491
|
),
|
|
492
|
+
"simulate_component": EndpointMapping(
|
|
493
|
+
method="GET",
|
|
494
|
+
path="/api/simulate",
|
|
495
|
+
request_transformer=_simulate_component_request,
|
|
496
|
+
),
|
|
497
|
+
"get_port_center": EndpointMapping(
|
|
498
|
+
method="GET",
|
|
499
|
+
path="/api/port-center",
|
|
500
|
+
request_transformer=_get_port_center_request,
|
|
501
|
+
),
|
|
502
|
+
"generate_bbox": EndpointMapping(
|
|
503
|
+
method="POST",
|
|
504
|
+
path="/api/bbox",
|
|
505
|
+
request_transformer=_generate_bbox_request,
|
|
506
|
+
),
|
|
507
|
+
"freeze_cell": EndpointMapping(
|
|
508
|
+
method="POST",
|
|
509
|
+
path="/freeze/{cell_name}",
|
|
510
|
+
request_transformer=_freeze_cell_request,
|
|
511
|
+
),
|
|
512
|
+
"get_pdk_info": EndpointMapping(
|
|
513
|
+
method="GET",
|
|
514
|
+
path="/info",
|
|
515
|
+
),
|
|
237
516
|
}
|
|
238
517
|
|
|
239
518
|
|
mcp_standalone/registry.py
CHANGED
|
@@ -102,8 +102,6 @@ class ServerInfo:
|
|
|
102
102
|
If psutil is not available, always returns True
|
|
103
103
|
"""
|
|
104
104
|
if not HAS_PSUTIL:
|
|
105
|
-
# Without psutil, assume the process is alive
|
|
106
|
-
# The registry cleanup is handled by gdsfactoryplus
|
|
107
105
|
return True
|
|
108
106
|
|
|
109
107
|
try:
|
|
@@ -163,7 +161,6 @@ class ServerRegistry:
|
|
|
163
161
|
|
|
164
162
|
server_info = ServerInfo.from_dict(data["servers"][port_key])
|
|
165
163
|
|
|
166
|
-
# Check if process is still alive (if psutil is available)
|
|
167
164
|
if HAS_PSUTIL and not server_info.is_alive():
|
|
168
165
|
return None
|
|
169
166
|
|
|
@@ -203,7 +200,6 @@ class ServerRegistry:
|
|
|
203
200
|
for server_data in data["servers"].values():
|
|
204
201
|
server_info = ServerInfo.from_dict(server_data)
|
|
205
202
|
|
|
206
|
-
# Include all servers if psutil is not available or include_dead is True
|
|
207
203
|
if not HAS_PSUTIL or include_dead or server_info.is_alive():
|
|
208
204
|
servers.append(server_info)
|
|
209
205
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""MCP resource definitions for GDSFactory+.
|
|
2
|
+
|
|
3
|
+
This module defines the MCP resources that are exposed to AI assistants.
|
|
4
|
+
Resources provide read-only access to documentation and instruction content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from mcp.types import Resource
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RESOURCES",
|
|
13
|
+
"get_all_resources",
|
|
14
|
+
"get_resource_content",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
RESOURCES: list[Resource] = [
|
|
19
|
+
Resource(
|
|
20
|
+
uri="instructions://build_custom_cell",
|
|
21
|
+
name="Build Custom Cell Instructions",
|
|
22
|
+
description=(
|
|
23
|
+
"Comprehensive instructions for building custom photonic cells "
|
|
24
|
+
"in GDSFactory+. Covers component creation, file organization, "
|
|
25
|
+
"PDK integration, and best practices."
|
|
26
|
+
),
|
|
27
|
+
mimeType="text/markdown",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RESOURCE_CONTENT = {
|
|
33
|
+
"instructions://build_custom_cell": """---
|
|
34
|
+
Workflow: Creating Custom Components in GDSFactory+ Projects
|
|
35
|
+
|
|
36
|
+
Overview
|
|
37
|
+
|
|
38
|
+
When users request custom photonic components, keep user-defined components separate from PDK-defined
|
|
39
|
+
objects to maintain clean project organization.
|
|
40
|
+
|
|
41
|
+
Step-by-Step Process
|
|
42
|
+
|
|
43
|
+
1. File Organization
|
|
44
|
+
|
|
45
|
+
- DO NOT add custom components to cells.py - this file is reserved for PDK-defined objects
|
|
46
|
+
- CREATE or use custom_components.py in the package directory (e.g., myph18da/custom_components.py)
|
|
47
|
+
- This keeps custom user components separate from the PDK
|
|
48
|
+
|
|
49
|
+
2. Component Creation
|
|
50
|
+
|
|
51
|
+
\"\"\"Custom user-defined components.\"\"\"
|
|
52
|
+
|
|
53
|
+
import gdsfactory as gf
|
|
54
|
+
from ph18da.cells import <relevant_pdk_functions> # Use PDK functions when available
|
|
55
|
+
|
|
56
|
+
__all__ = ["custom_component_name"]
|
|
57
|
+
|
|
58
|
+
@gf.cell
|
|
59
|
+
def custom_component_name(
|
|
60
|
+
param1: float = default_value,
|
|
61
|
+
param2: float = default_value,
|
|
62
|
+
) -> gf.Component:
|
|
63
|
+
\"\"\"Component description.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
param1: Description
|
|
67
|
+
param2: Description
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Component description
|
|
71
|
+
\"\"\"
|
|
72
|
+
c = pdk_function(parameters...)
|
|
73
|
+
return c
|
|
74
|
+
|
|
75
|
+
3. Key Implementation Guidelines
|
|
76
|
+
|
|
77
|
+
- Use @gf.cell decorator for all components
|
|
78
|
+
- Prefer using existing PDK functions (e.g., mzi_swg) over internal functions (e.g., _mzi)
|
|
79
|
+
- Import from ph18da.cells for PDK-specific components
|
|
80
|
+
- Use standard PDK component names (e.g., "c_mmi_2x2_swg", "wg_straight_swg", "wg_arc_swg")
|
|
81
|
+
- Keep component parameters clear and well-documented
|
|
82
|
+
|
|
83
|
+
4. Register the Component
|
|
84
|
+
|
|
85
|
+
Update the package __init__.py to import and expose the custom component:
|
|
86
|
+
from myph18da.custom_components import custom_component_name
|
|
87
|
+
|
|
88
|
+
5. Build the Component
|
|
89
|
+
|
|
90
|
+
Use the MCP build tool:
|
|
91
|
+
mcp__gdsfactoryplus__build_cell(
|
|
92
|
+
name="custom_component_name",
|
|
93
|
+
project="project-name"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
6. Verification
|
|
97
|
+
|
|
98
|
+
- Check build status with get_cell_info
|
|
99
|
+
- Verify GDS file exists in build/gds/ directory
|
|
100
|
+
- Ensure status is "SUCCESS"
|
|
101
|
+
|
|
102
|
+
Troubleshooting
|
|
103
|
+
|
|
104
|
+
If build fails with import errors:
|
|
105
|
+
- Use PDK public functions instead of internal _functions
|
|
106
|
+
- Check that imports are from the correct PDK modules
|
|
107
|
+
|
|
108
|
+
If component not found:
|
|
109
|
+
- Ensure it's imported in __init__.py
|
|
110
|
+
- Rebuild the component after changes
|
|
111
|
+
|
|
112
|
+
If parameter errors occur:
|
|
113
|
+
- Check PDK component compatibility
|
|
114
|
+
- Verify parameter names match PDK expectations
|
|
115
|
+
---
|
|
116
|
+
""",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_all_resources() -> list[Resource]:
|
|
121
|
+
"""Get all available MCP resources.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of all resource definitions
|
|
125
|
+
"""
|
|
126
|
+
return RESOURCES
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_resource_content(uri: str) -> str | None:
|
|
130
|
+
"""Get resource content by URI.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
uri: Resource URI
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Resource content string or None if not found
|
|
137
|
+
"""
|
|
138
|
+
return RESOURCE_CONTENT.get(uri)
|