gfp-mcp 0.2.1__py3-none-any.whl → 0.3.2__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/tools/drc.py ADDED
@@ -0,0 +1,379 @@
1
+ """DRC (Design Rule Check) tool handler.
2
+
3
+ This module provides the handler for running DRC checks on GDS files,
4
+ including XML parsing and actionable violation summaries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import xml.etree.ElementTree as ET
10
+ from typing import Any
11
+
12
+ from mcp.types import Tool
13
+
14
+ from .base import EndpointMapping, ToolHandler, add_project_param
15
+
16
+ __all__ = ["CheckDrcHandler"]
17
+
18
+
19
+ def _parse_polygon_location(values_elem: ET.Element | None) -> dict[str, Any] | None:
20
+ """Parse polygon coordinates and compute location data.
21
+
22
+ Args:
23
+ values_elem: XML element containing polygon coordinates
24
+
25
+ Returns:
26
+ Dict with bbox, centroid, and size, or None if parsing fails
27
+ """
28
+ if values_elem is None:
29
+ return None
30
+
31
+ value_elem = values_elem.find("value")
32
+ if value_elem is None or value_elem.text is None:
33
+ return None
34
+
35
+ value_text = value_elem.text
36
+ if "polygon:" not in value_text:
37
+ return None
38
+
39
+ try:
40
+ coords_str = value_text.replace("polygon:", "").strip().strip("()")
41
+ if not coords_str:
42
+ return None
43
+
44
+ pairs = coords_str.split(";")
45
+ coords = []
46
+ for pair in pairs:
47
+ if "," not in pair:
48
+ continue
49
+ x_str, y_str = pair.split(",", 1)
50
+ coords.append((float(x_str), float(y_str)))
51
+
52
+ if not coords:
53
+ return None
54
+
55
+ xs = [x for x, y in coords]
56
+ ys = [y for x, y in coords]
57
+
58
+ bbox = {
59
+ "min_x": round(min(xs), 3),
60
+ "min_y": round(min(ys), 3),
61
+ "max_x": round(max(xs), 3),
62
+ "max_y": round(max(ys), 3),
63
+ }
64
+
65
+ centroid = {
66
+ "x": round(sum(xs) / len(xs), 3),
67
+ "y": round(sum(ys) / len(ys), 3),
68
+ }
69
+
70
+ size = {
71
+ "width": round(bbox["max_x"] - bbox["min_x"], 3),
72
+ "height": round(bbox["max_y"] - bbox["min_y"], 3),
73
+ }
74
+
75
+ return {
76
+ "bbox": bbox,
77
+ "centroid": centroid,
78
+ "size": size,
79
+ }
80
+
81
+ except (ValueError, IndexError):
82
+ return None
83
+
84
+
85
+ def _generate_drc_recommendations(
86
+ total_violations: int,
87
+ violations_by_category: list[dict[str, Any]],
88
+ cells: list[str],
89
+ ) -> list[str]:
90
+ """Generate actionable recommendations based on DRC results.
91
+
92
+ Args:
93
+ total_violations: Total number of violations
94
+ violations_by_category: List of violation categories with counts
95
+ cells: List of cells checked
96
+
97
+ Returns:
98
+ List of recommendation strings
99
+ """
100
+ if total_violations == 0:
101
+ return ["Design passes all DRC checks"]
102
+
103
+ recommendations = []
104
+
105
+ if violations_by_category:
106
+ top_category = violations_by_category[0]
107
+ if top_category["count"] > 10:
108
+ recommendations.append(
109
+ f"Focus on {top_category['category']} violations "
110
+ f"({top_category['count']:,} occurrences)"
111
+ )
112
+ elif top_category["count"] > 1:
113
+ recommendations.append(
114
+ f"Check {top_category['category']} rules in {', '.join(cells)}"
115
+ )
116
+
117
+ if len(violations_by_category) > 1:
118
+ recommendations.append(
119
+ f"Review {len(violations_by_category)} different DRC rule categories"
120
+ )
121
+
122
+ if total_violations > 100:
123
+ recommendations.append(
124
+ "Consider fixing systematic issues first to reduce violation count"
125
+ )
126
+
127
+ return recommendations
128
+
129
+
130
+ class CheckDrcHandler(ToolHandler):
131
+ """Handler for running DRC checks on GDS files.
132
+
133
+ Parses KLayout RDB XML and extracts actionable violation summary
134
+ without polygon coordinates or verbose metadata. Computes simplified
135
+ location data (bounding box, centroid, size) from polygon coordinates
136
+ to reduce token usage while preserving all violation details.
137
+ """
138
+
139
+ @property
140
+ def name(self) -> str:
141
+ return "check_drc"
142
+
143
+ @property
144
+ def definition(self) -> Tool:
145
+ return Tool(
146
+ name="check_drc",
147
+ description=(
148
+ "Run a full DRC (Design Rule Check) on a GDS file. This uploads "
149
+ "the file to a remote DRC server and runs comprehensive design rule "
150
+ "verification for the specified PDK and process. Use this for "
151
+ "complete design rule validation. Returns XML results showing all "
152
+ "DRC violations."
153
+ ),
154
+ inputSchema=add_project_param(
155
+ {
156
+ "type": "object",
157
+ "properties": {
158
+ "path": {
159
+ "type": "string",
160
+ "description": (
161
+ "Path to the GDS file to check. Can be absolute or "
162
+ "relative to the project directory."
163
+ ),
164
+ },
165
+ "pdk": {
166
+ "type": "string",
167
+ "description": (
168
+ "PDK to use for the check. If not specified, uses the "
169
+ "default PDK from settings."
170
+ ),
171
+ },
172
+ "process": {
173
+ "type": "string",
174
+ "description": (
175
+ "Process variant for DRC rules. If not specified, uses "
176
+ "the default process from settings."
177
+ ),
178
+ },
179
+ "timeout": {
180
+ "type": "integer",
181
+ "description": (
182
+ "Timeout in seconds for the DRC check. If not specified, "
183
+ "uses the default timeout from settings."
184
+ ),
185
+ },
186
+ "host": {
187
+ "type": "string",
188
+ "description": (
189
+ "API host for the DRC server. If not specified, uses "
190
+ "the default host from settings."
191
+ ),
192
+ },
193
+ },
194
+ "required": ["path"],
195
+ }
196
+ ),
197
+ )
198
+
199
+ @property
200
+ def mapping(self) -> EndpointMapping:
201
+ return EndpointMapping(method="POST", path="/api/check-drc")
202
+
203
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
204
+ """Transform check_drc MCP args to FastAPI params.
205
+
206
+ Args:
207
+ args: MCP tool arguments
208
+
209
+ Returns:
210
+ Dict with 'json_data' key for request body
211
+ """
212
+ json_data: dict[str, Any] = {"path": args["path"]}
213
+
214
+ if "pdk" in args and args["pdk"]:
215
+ json_data["pdk"] = args["pdk"]
216
+ if "process" in args and args["process"]:
217
+ json_data["process"] = args["process"]
218
+ if "timeout" in args and args["timeout"]:
219
+ json_data["timeout"] = args["timeout"]
220
+ if "host" in args and args["host"]:
221
+ json_data["host"] = args["host"]
222
+
223
+ return {"json_data": json_data}
224
+
225
+ def transform_response(self, response: Any) -> dict[str, Any]:
226
+ """Transform check_drc response to LLM-friendly format.
227
+
228
+ Parses KLayout RDB XML and extracts actionable violation summary
229
+ without polygon coordinates or verbose metadata. Computes simplified
230
+ location data (bounding box, centroid, size) from polygon coordinates
231
+ to reduce token usage by ~82% while preserving all violation details.
232
+
233
+ Args:
234
+ response: FastAPI response (XML string or dict with 'content' key)
235
+
236
+ Returns:
237
+ Structured summary with all violations including location data,
238
+ or error dict if parsing fails
239
+ """
240
+ try:
241
+ # Handle FastAPI error responses (e.g., file not found)
242
+ if isinstance(response, dict) and "detail" in response:
243
+ detail = response["detail"]
244
+ # Check for common error patterns and provide helpful messages
245
+ if "does not exist" in str(detail).lower():
246
+ return {
247
+ "error": "File not found",
248
+ "detail": detail,
249
+ "suggestion": (
250
+ "The GDS file does not exist. Please build the cell first "
251
+ "using 'build_cells' before running DRC."
252
+ ),
253
+ }
254
+ return {
255
+ "error": "DRC check failed",
256
+ "detail": detail,
257
+ }
258
+
259
+ if isinstance(response, dict) and "content" in response:
260
+ xml_str = response["content"]
261
+ elif isinstance(response, str):
262
+ xml_str = response
263
+ else:
264
+ return {
265
+ "error": f"Unexpected response type: {type(response).__name__}",
266
+ "suggestion": "Expected XML string or dict with 'content' key",
267
+ "response_preview": str(response)[:500],
268
+ }
269
+ except Exception as e:
270
+ return {
271
+ "error": f"Failed to extract XML from response: {e}",
272
+ "response_preview": str(response)[:500],
273
+ }
274
+
275
+ try:
276
+ root = ET.fromstring(xml_str)
277
+ except ET.ParseError as e:
278
+ return {
279
+ "error": f"Failed to parse DRC XML: {e}",
280
+ "raw_preview": xml_str[:500],
281
+ "suggestion": "Check if DRC server returned valid XML",
282
+ }
283
+
284
+ categories_map = {}
285
+ for category in root.findall(".//categories/category"):
286
+ name_elem = category.find("name")
287
+ desc_elem = category.find("description")
288
+ if name_elem is not None and name_elem.text:
289
+ categories_map[name_elem.text] = (
290
+ desc_elem.text
291
+ if desc_elem is not None and desc_elem.text
292
+ else name_elem.text
293
+ )
294
+
295
+ cells = []
296
+ for cell in root.findall(".//cells/cell"):
297
+ name_elem = cell.find("name")
298
+ if name_elem is not None and name_elem.text:
299
+ cells.append(name_elem.text)
300
+
301
+ violations = []
302
+ violation_counts: dict[str, int] = {}
303
+
304
+ for item in root.findall(".//items/item"):
305
+ category_elem = item.find("category")
306
+ cell_elem = item.find("cell")
307
+ comment_elem = item.find("comment")
308
+ values_elem = item.find("values")
309
+
310
+ if category_elem is None or category_elem.text is None:
311
+ continue
312
+
313
+ category = category_elem.text
314
+ cell = (
315
+ cell_elem.text
316
+ if cell_elem is not None and cell_elem.text
317
+ else "unknown"
318
+ )
319
+ description = (
320
+ comment_elem.text
321
+ if comment_elem is not None and comment_elem.text
322
+ else categories_map.get(category, category)
323
+ )
324
+
325
+ location = _parse_polygon_location(values_elem)
326
+
327
+ violation = {
328
+ "category": category,
329
+ "cell": cell,
330
+ "description": description,
331
+ }
332
+
333
+ if location is not None:
334
+ violation["location"] = location
335
+ elif values_elem is not None:
336
+ violation["location_warning"] = "Could not parse coordinates"
337
+
338
+ violations.append(violation)
339
+
340
+ violation_counts[category] = violation_counts.get(category, 0) + 1
341
+
342
+ total_violations = len(violations)
343
+ status = "PASSED" if total_violations == 0 else "FAILED"
344
+
345
+ summary = {
346
+ "total_violations": total_violations,
347
+ "total_categories": len(violation_counts),
348
+ "cells_checked": cells,
349
+ "status": status,
350
+ }
351
+
352
+ if total_violations == 0:
353
+ summary["message"] = "No DRC violations found"
354
+
355
+ violations_by_category = [
356
+ {
357
+ "category": cat,
358
+ "description": categories_map.get(cat, cat),
359
+ "count": count,
360
+ }
361
+ for cat, count in sorted(
362
+ violation_counts.items(),
363
+ key=lambda x: x[1],
364
+ reverse=True,
365
+ )
366
+ ]
367
+
368
+ recommendations = _generate_drc_recommendations(
369
+ total_violations,
370
+ violations_by_category,
371
+ cells,
372
+ )
373
+
374
+ return {
375
+ "summary": summary,
376
+ "violations_by_category": violations_by_category,
377
+ "violations": violations,
378
+ "recommendations": recommendations,
379
+ }
@@ -0,0 +1,82 @@
1
+ """Freeze cell tool handler.
2
+
3
+ This module provides the handler for freezing parametric Python cells
4
+ as static schematic netlists.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["FreezeCellHandler"]
16
+
17
+
18
+ class FreezeCellHandler(ToolHandler):
19
+ """Handler for freezing parametric cells.
20
+
21
+ Converts a gdsfactory component with specific parameters into
22
+ a fixed netlist representation in YAML format.
23
+ """
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "freeze_cell"
28
+
29
+ @property
30
+ def definition(self) -> Tool:
31
+ return Tool(
32
+ name="freeze_cell",
33
+ description=(
34
+ "Freeze a parametric Python cell as a static schematic netlist. "
35
+ "This converts a gdsfactory component with specific parameters into "
36
+ "a fixed netlist representation in YAML format. Useful for creating "
37
+ "versioned snapshots of parametric designs or preparing components "
38
+ "for simulation workflows."
39
+ ),
40
+ inputSchema=add_project_param(
41
+ {
42
+ "type": "object",
43
+ "properties": {
44
+ "cell_name": {
45
+ "type": "string",
46
+ "description": "Name of the cell/component to freeze",
47
+ },
48
+ "kwargs": {
49
+ "type": "object",
50
+ "description": (
51
+ "Optional keyword arguments to pass to the component "
52
+ "factory. Use this to specify parameter values when "
53
+ "freezing the cell. Default is empty (use default params)."
54
+ ),
55
+ "default": {},
56
+ },
57
+ },
58
+ "required": ["cell_name"],
59
+ }
60
+ ),
61
+ )
62
+
63
+ @property
64
+ def mapping(self) -> EndpointMapping:
65
+ return EndpointMapping(method="POST", path="/freeze/{cell_name}")
66
+
67
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
68
+ """Transform freeze_cell MCP args to FastAPI params.
69
+
70
+ Args:
71
+ args: MCP tool arguments
72
+
73
+ Returns:
74
+ Dict with 'path' and 'json_data' for the request
75
+ """
76
+ cell_name = args["cell_name"]
77
+ kwargs = args.get("kwargs", {})
78
+
79
+ return {
80
+ "path": f"/freeze/{cell_name}",
81
+ "json_data": kwargs, # httpx will JSON-encode this to a string
82
+ }
gfp_mcp/tools/lvs.py ADDED
@@ -0,0 +1,86 @@
1
+ """LVS (Layout vs. Schematic) check tool handler.
2
+
3
+ This module provides the handler for running LVS verification
4
+ on cells against reference netlists.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["CheckLvsHandler"]
16
+
17
+
18
+ class CheckLvsHandler(ToolHandler):
19
+ """Handler for running LVS verification.
20
+
21
+ Compares physical layout to schematic representation
22
+ to ensure they match.
23
+ """
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "check_lvs"
28
+
29
+ @property
30
+ def definition(self) -> Tool:
31
+ return Tool(
32
+ name="check_lvs",
33
+ description=(
34
+ "Run LVS (Layout vs. Schematic) verification on a cell against a "
35
+ "reference netlist. This compares the physical layout to the "
36
+ "schematic representation to ensure they match. Returns XML results "
37
+ "showing any mismatches between layout and schematic."
38
+ ),
39
+ inputSchema=add_project_param(
40
+ {
41
+ "type": "object",
42
+ "properties": {
43
+ "cell": {
44
+ "type": "string",
45
+ "description": "Name of the cell to verify",
46
+ },
47
+ "netpath": {
48
+ "type": "string",
49
+ "description": (
50
+ "Path to the reference netlist file to compare against"
51
+ ),
52
+ },
53
+ "cellargs": {
54
+ "type": "string",
55
+ "description": (
56
+ "Optional cell arguments as a JSON string. "
57
+ "Default is empty string."
58
+ ),
59
+ "default": "",
60
+ },
61
+ },
62
+ "required": ["cell", "netpath"],
63
+ }
64
+ ),
65
+ )
66
+
67
+ @property
68
+ def mapping(self) -> EndpointMapping:
69
+ return EndpointMapping(method="POST", path="/api/check-lvs")
70
+
71
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
72
+ """Transform check_lvs MCP args to FastAPI params.
73
+
74
+ Args:
75
+ args: MCP tool arguments
76
+
77
+ Returns:
78
+ Dict with 'json_data' key for request body
79
+ """
80
+ return {
81
+ "json_data": {
82
+ "cell": args["cell"],
83
+ "netpath": args["netpath"],
84
+ "cellargs": args.get("cellargs", ""),
85
+ }
86
+ }
gfp_mcp/tools/pdk.py ADDED
@@ -0,0 +1,47 @@
1
+ """PDK info tool handler.
2
+
3
+ This module provides the handler for getting information about
4
+ the current PDK (Process Design Kit) in use.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mcp.types import Tool
10
+
11
+ from .base import EndpointMapping, ToolHandler, add_project_param
12
+
13
+ __all__ = ["GetPdkInfoHandler"]
14
+
15
+
16
+ class GetPdkInfoHandler(ToolHandler):
17
+ """Handler for getting PDK information.
18
+
19
+ Returns metadata including PDK name, project name, project path,
20
+ server port, and version.
21
+ """
22
+
23
+ @property
24
+ def name(self) -> str:
25
+ return "get_pdk_info"
26
+
27
+ @property
28
+ def definition(self) -> Tool:
29
+ return Tool(
30
+ name="get_pdk_info",
31
+ description=(
32
+ "Get information about the current PDK (Process Design Kit) in use. "
33
+ "Returns metadata including PDK name, project name, project path, "
34
+ "server port, and version. Use this to verify which PDK is active "
35
+ "and get project configuration details."
36
+ ),
37
+ inputSchema=add_project_param(
38
+ {
39
+ "type": "object",
40
+ "properties": {},
41
+ }
42
+ ),
43
+ )
44
+
45
+ @property
46
+ def mapping(self) -> EndpointMapping:
47
+ return EndpointMapping(method="GET", path="/info")
gfp_mcp/tools/port.py ADDED
@@ -0,0 +1,82 @@
1
+ """Port center coordinate tool handler.
2
+
3
+ This module provides the handler for getting port center coordinates
4
+ from component instances.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["GetPortCenterHandler"]
16
+
17
+
18
+ class GetPortCenterHandler(ToolHandler):
19
+ """Handler for getting port center coordinates.
20
+
21
+ Returns the physical coordinates (x, y) of a specific port
22
+ in a component instance.
23
+ """
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "get_port_center"
28
+
29
+ @property
30
+ def definition(self) -> Tool:
31
+ return Tool(
32
+ name="get_port_center",
33
+ description=(
34
+ "Get the center coordinates (x, y) of a specific port in a component "
35
+ "instance. This is useful for positioning components, routing waveguides, "
36
+ "or analyzing layout geometry. Returns the physical coordinates in "
37
+ "microns."
38
+ ),
39
+ inputSchema=add_project_param(
40
+ {
41
+ "type": "object",
42
+ "properties": {
43
+ "netlist": {
44
+ "type": "string",
45
+ "description": (
46
+ "Name of the component/netlist containing the instance"
47
+ ),
48
+ },
49
+ "instance": {
50
+ "type": "string",
51
+ "description": "Name of the instance within the netlist",
52
+ },
53
+ "port": {
54
+ "type": "string",
55
+ "description": "Name of the port to get coordinates for",
56
+ },
57
+ },
58
+ "required": ["netlist", "instance", "port"],
59
+ }
60
+ ),
61
+ )
62
+
63
+ @property
64
+ def mapping(self) -> EndpointMapping:
65
+ return EndpointMapping(method="GET", path="/api/port-center")
66
+
67
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
68
+ """Transform get_port_center MCP args to FastAPI params.
69
+
70
+ Args:
71
+ args: MCP tool arguments
72
+
73
+ Returns:
74
+ Dict with 'params' key for query parameters
75
+ """
76
+ return {
77
+ "params": {
78
+ "netlist": args["netlist"],
79
+ "instance": args["instance"],
80
+ "port": args["port"],
81
+ }
82
+ }