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.
@@ -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 _download_gds_request(args: dict[str, Any]) -> dict[str, Any]:
109
- """Transform download_gds MCP args to FastAPI path.
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 modified 'path' for the endpoint
98
+ Dict with 'json_data' key for request body
116
99
  """
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"}
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 _download_gds_response(response: Any) -> dict[str, Any]:
124
- """Transform download_gds response to MCP format.
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 (file path or error)
123
+ response: FastAPI response (XML string or dict with 'content' key)
128
124
 
129
125
  Returns:
130
- Formatted response with file path
126
+ Structured summary with all violations including location data,
127
+ or error dict if parsing fails
131
128
  """
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
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
- def _check_drc_request(args: dict[str, Any]) -> dict[str, Any]:
139
- """Transform check_drc MCP args to FastAPI params.
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
- args: MCP tool arguments
252
+ values_elem: XML element containing polygon coordinates
143
253
 
144
254
  Returns:
145
- Dict with 'json_data' key for request body
255
+ Dict with bbox, centroid, and size, or None if parsing fails
146
256
  """
147
- json_data: dict[str, Any] = {"path": args["path"]}
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
- # 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"]
294
+ centroid = {
295
+ "x": round(sum(xs) / len(xs), 3),
296
+ "y": round(sum(ys) / len(ys), 3),
297
+ }
158
298
 
159
- return {"json_data": json_data}
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
- # Tool name -> Endpoint mapping
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
 
@@ -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)