gfp-mcp 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gfp-mcp
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: Model Context Protocol (MCP) server for GDSFactory+ photonic IC design
5
5
  Author: GDSFactory+ Team
6
6
  License: MIT
@@ -33,13 +33,15 @@ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
33
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
35
35
  Requires-Dist: ruff>=0.1.0; extra == "dev"
36
+ Requires-Dist: bump-my-version>=0.26.0; extra == "dev"
37
+ Requires-Dist: build>=1.4.0; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
38
40
  # GDSFactory+ MCP Server
39
41
 
40
42
  [![PyPI version](https://img.shields.io/pypi/v/gfp-mcp.svg)](https://pypi.org/project/gfp-mcp/)
41
43
  [![Python versions](https://img.shields.io/pypi/pyversions/gfp-mcp.svg)](https://pypi.org/project/gfp-mcp/)
42
- [![Tests](https://github.com/doplaydo/gfp-mcp/workflows/Tests/badge.svg)](https://github.com/doplaydo/gfp-mcp/actions)
44
+ [![Tests](https://github.com/doplaydo/gfp-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/doplaydo/gfp-mcp/actions)
43
45
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
46
 
45
47
  Model Context Protocol (MCP) server for GDSFactory+ that enables AI assistants like Claude to design and build photonic integrated circuits.
@@ -57,15 +59,32 @@ This MCP server connects AI assistants to [GDSFactory+](https://gdsfactory.com),
57
59
 
58
60
  ### 2. Install the MCP Server
59
61
 
62
+ **With uv (recommended):**
63
+
64
+ If you don't have `uv` installed, see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
65
+
60
66
  ```bash
61
- pip install gfp-mcp
67
+ uv tool install gfp-mcp
62
68
  ```
63
69
 
64
- Or with uv:
70
+ <details>
71
+ <summary>Ephemeral approach</summary>
72
+
73
+ Run without installing:
65
74
 
66
75
  ```bash
67
76
  uvx --from gfp-mcp gfp-mcp-serve
68
77
  ```
78
+ </details>
79
+
80
+ <details>
81
+ <summary><strong>Alternative: pip install</strong></summary>
82
+
83
+ ```bash
84
+ pip install gfp-mcp
85
+ ```
86
+
87
+ </details>
69
88
 
70
89
  ### 3. Connect to Your AI Assistant
71
90
 
@@ -143,14 +162,12 @@ Try these commands with your AI assistant:
143
162
 
144
163
  ## Available Tools
145
164
 
146
- - **build_cell** - Build a single GDS cell by name
147
- - **build_cells** - Build multiple GDS cells in batch
165
+ - **build_cells** - Build one or more GDS cells by name (pass a list, can be single-item)
148
166
  - **list_cells** - List all available photonic components
149
167
  - **get_cell_info** - Get detailed component metadata
150
- - **download_gds** - Download built GDS files
151
168
  - **list_projects** - List all running GDSFactory+ server instances
152
169
  - **get_project_info** - Get detailed information about a specific project
153
- - **check_drc** - Run Design Rule Check verification
170
+ - **check_drc** - Run Design Rule Check verification (returns structured format with all violations including simplified location data for LLM-friendly troubleshooting)
154
171
  - **check_connectivity** - Run connectivity verification
155
172
  - **check_lvs** - Run Layout vs. Schematic verification
156
173
 
@@ -0,0 +1,14 @@
1
+ gfp_mcp-0.2.4.dist-info/licenses/LICENSE,sha256=ixSuHdKKXzNJw_eTgAxHzaCNIds8k48hytA_eJgA8gQ,225
2
+ mcp_standalone/__init__.py,sha256=1-BT202aWn5Uwt-5bDHyGtBw3ObSKhK9ATzQlIXiJdw,1069
3
+ mcp_standalone/client.py,sha256=LWO1emsiUa4Dg9yXH0FO7-LHV2ngxOov_acwH7JnVFo,9902
4
+ mcp_standalone/config.py,sha256=bxJXYioVhx5FnS_dzvIEyVjaQbC91s6Pkl0tCRms3Ig,1225
5
+ mcp_standalone/mappings.py,sha256=LFjo8Q5olO3LQM4Jy4h6xa4CD3bMWlyG_RtfDJGvQx4,15928
6
+ mcp_standalone/registry.py,sha256=ozDrMPksWoyKAkNd9MH2g8FRoN0XEQKWnLwZsgDxWLc,6127
7
+ mcp_standalone/resources.py,sha256=upY93XVemc5ezx1p_YML57O6HYVahCs2RsYXn4Dw_X0,3517
8
+ mcp_standalone/server.py,sha256=FSjGSjwpe_D2idMrRrAMTq9pnwBToK2vZ1VtzPIMyeg,8788
9
+ mcp_standalone/tools.py,sha256=qQAI6-Xb0sWUTJyYz5a47830cerbjwO5euIRhDmBRUk,16468
10
+ gfp_mcp-0.2.4.dist-info/METADATA,sha256=v99aAe-JU0aQ7pmzICvHhoGLaKv4GoG4F9fNkxJHbyw,7369
11
+ gfp_mcp-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ gfp_mcp-0.2.4.dist-info/entry_points.txt,sha256=mgyus9dsB_8mjgnztuHNPqzPi-7HcPg1iYzfM5NMIjk,61
13
+ gfp_mcp-0.2.4.dist-info/top_level.txt,sha256=g2hRJHoDDPNtrNdXR70T7FR9Ev6DTRJiGW7ZvlvnXMc,15
14
+ gfp_mcp-0.2.4.dist-info/RECORD,,
@@ -39,4 +39,4 @@ __all__ = [
39
39
  "get_resource_content",
40
40
  ]
41
41
 
42
- __version__ = "0.2.1"
42
+ __version__ = "0.2.4"
mcp_standalone/client.py CHANGED
@@ -43,6 +43,21 @@ class FastAPIClient:
43
43
  self._client: httpx.AsyncClient | None = None
44
44
  self._registry = ServerRegistry()
45
45
 
46
+ def _has_available_servers(self) -> bool:
47
+ """Check if any servers are available in the registry."""
48
+ return len(self._registry.list_servers()) > 0
49
+
50
+ def _get_default_server_url(self) -> str | None:
51
+ """Get the first available server URL from registry if no base_url configured."""
52
+ if self.base_url:
53
+ return self.base_url
54
+
55
+ servers = self._registry.list_servers()
56
+ if servers:
57
+ return f"http://localhost:{servers[0].port}"
58
+
59
+ return None
60
+
46
61
  async def __aenter__(self) -> Self:
47
62
  """Enter async context."""
48
63
  await self.start()
@@ -55,12 +70,17 @@ class FastAPIClient:
55
70
  async def start(self) -> None:
56
71
  """Start the HTTP client with connection pooling."""
57
72
  if self._client is None:
73
+ base_url = self.base_url or "http://localhost"
74
+
58
75
  self._client = httpx.AsyncClient(
59
- base_url=self.base_url,
76
+ base_url=base_url,
60
77
  timeout=httpx.Timeout(self.timeout),
61
78
  limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
62
79
  )
63
- logger.debug("HTTP client started with base URL: %s", self.base_url)
80
+ logger.debug(
81
+ "HTTP client started with base URL: %s (resolved per-request if needed)",
82
+ self.base_url or "from registry",
83
+ )
64
84
 
65
85
  async def close(self) -> None:
66
86
  """Close the HTTP client."""
@@ -79,22 +99,46 @@ class FastAPIClient:
79
99
  Base URL for the request
80
100
 
81
101
  Raises:
82
- ValueError: If project not found in registry
102
+ ValueError: If base URL cannot be resolved
83
103
  """
84
- # If no project specified, use default base_url
85
- if project is None:
104
+ if project is not None:
105
+ server_info = self._registry.get_server_by_project(project)
106
+ if server_info is None:
107
+ available = self._registry.list_servers()
108
+ if available:
109
+ project_list = ", ".join([s.project_name for s in available[:3]])
110
+ if len(available) > 3:
111
+ project_list += f", ... and {len(available) - 3} more"
112
+ msg = (
113
+ f"Project '{project}' not found in registry. "
114
+ f"Available projects: {project_list}. "
115
+ "Use list_projects tool to see all running servers."
116
+ )
117
+ else:
118
+ msg = (
119
+ f"Project '{project}' not found. No GDSFactory+ servers are running. "
120
+ "Please open a GDSFactory+ project in VSCode with the extension installed."
121
+ )
122
+ raise ValueError(msg)
123
+
124
+ return f"http://localhost:{server_info.port}"
125
+
126
+ if self.base_url:
86
127
  return self.base_url
87
128
 
88
- # Look up project in registry
89
- server_info = self._registry.get_server_by_project(project)
90
- if server_info is None:
91
- msg = (
92
- f"Project '{project}' not found in registry. "
93
- "Make sure the server is running for this project."
129
+ default_url = self._get_default_server_url()
130
+ if default_url:
131
+ logger.info(
132
+ "No project specified, using first available server: %s", default_url
94
133
  )
95
- raise ValueError(msg)
134
+ return default_url
96
135
 
97
- return f"http://localhost:{server_info.port}"
136
+ msg = (
137
+ "No project specified and no GDSFactory+ servers are running. "
138
+ "Either: (1) Start a server by opening a GDSFactory+ project in VSCode, "
139
+ "(2) Specify a project parameter, or (3) Set GFP_API_URL environment variable."
140
+ )
141
+ raise ValueError(msg)
98
142
 
99
143
  async def request(
100
144
  self,
@@ -125,7 +169,6 @@ class FastAPIClient:
125
169
  if self._client is None:
126
170
  await self.start()
127
171
 
128
- # Resolve the base URL for this request
129
172
  base_url = self._resolve_base_url(project)
130
173
 
131
174
  last_error = None
@@ -143,7 +186,6 @@ class FastAPIClient:
143
186
  base_url,
144
187
  )
145
188
 
146
- # Build full URL with the resolved base URL
147
189
  full_url = f"{base_url}{path}"
148
190
 
149
191
  response = await self._client.request( # type: ignore[union-attr]
@@ -155,7 +197,6 @@ class FastAPIClient:
155
197
  )
156
198
  response.raise_for_status()
157
199
 
158
- # Try to parse JSON, fall back to text
159
200
  try:
160
201
  return response.json()
161
202
  except (ValueError, TypeError):
@@ -165,19 +206,16 @@ class FastAPIClient:
165
206
  last_error = e
166
207
  logger.warning("Request failed (attempt %d): %s", attempt + 1, e)
167
208
 
168
- # Don't retry on client errors (4xx)
169
209
  if (
170
210
  isinstance(e, httpx.HTTPStatusError)
171
211
  and 400 <= e.response.status_code < 500
172
212
  ):
173
213
  raise
174
214
 
175
- # Exponential backoff for retries
176
215
  if attempt < MCPConfig.MAX_RETRIES - 1:
177
216
  await asyncio.sleep(backoff)
178
217
  backoff *= 2
179
218
 
180
- # All retries failed
181
219
  logger.error("All %d attempts failed", MCPConfig.MAX_RETRIES)
182
220
  raise last_error # type: ignore[misc]
183
221
 
mcp_standalone/config.py CHANGED
@@ -15,34 +15,28 @@ class MCPConfig:
15
15
  that proxies requests to the FastAPI backend.
16
16
  """
17
17
 
18
- # FastAPI base URL (default: http://localhost:8787)
19
- # This default is primarily for backward compatibility.
20
- # The MCP server automatically discovers running servers via the registry.
21
- API_URL: Final[str] = os.getenv("GFP_API_URL", "http://localhost:8787")
18
+ API_URL: Final[str | None] = os.getenv("GFP_API_URL")
22
19
 
23
- # Timeout for tool calls in seconds (default: 300 = 5 minutes)
24
20
  TIMEOUT: Final[int] = int(os.getenv("GFP_MCP_TIMEOUT", "300"))
25
21
 
26
- # Enable debug logging
27
22
  DEBUG: Final[bool] = os.getenv("GFP_MCP_DEBUG", "false").lower() in (
28
23
  "true",
29
24
  "1",
30
25
  "yes",
31
26
  )
32
27
 
33
- # Retry configuration
34
28
  MAX_RETRIES: Final[int] = 3
35
29
  RETRY_BACKOFF: Final[float] = 0.5 # Initial backoff in seconds
36
30
 
37
31
  @classmethod
38
- def get_api_url(cls, override: str | None = None) -> str:
32
+ def get_api_url(cls, override: str | None = None) -> str | None:
39
33
  """Get the FastAPI base URL.
40
34
 
41
35
  Args:
42
36
  override: Optional URL to override the environment variable
43
37
 
44
38
  Returns:
45
- The API base URL
39
+ The API base URL or None if not configured
46
40
  """
47
41
  return override or cls.API_URL
48
42
 
@@ -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"]}
121
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"]
122
110
 
123
- def _download_gds_response(response: Any) -> dict[str, Any]:
124
- """Transform download_gds response to MCP format.
111
+ return {"json_data": json_data}
112
+
113
+
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
+ }
137
153
 
138
- def _check_drc_request(args: dict[str, Any]) -> dict[str, Any]:
139
- """Transform check_drc MCP args to FastAPI params.
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
+ }
246
+
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
+ }
298
+
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
158
312
 
159
- return {"json_data": json_data}
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]:
@@ -230,7 +427,6 @@ def _generate_bbox_request(args: dict[str, Any]) -> dict[str, Any]:
230
427
  """
231
428
  json_data: dict[str, Any] = {"path": args["path"]}
232
429
 
233
- # Add optional parameters if provided
234
430
  if "outpath" in args and args["outpath"]:
235
431
  json_data["outpath"] = args["outpath"]
236
432
  if "layers_to_keep" in args and args["layers_to_keep"]:
@@ -255,23 +451,13 @@ def _freeze_cell_request(args: dict[str, Any]) -> dict[str, Any]:
255
451
  cell_name = args["cell_name"]
256
452
  kwargs = args.get("kwargs", {})
257
453
 
258
- # The freeze endpoint expects a JSON string in the body.
259
- # httpx with json= will JSON-encode the dict and send it as the body,
260
- # which FastAPI's Body() will read as a string.
261
454
  return {
262
455
  "path": f"/freeze/{cell_name}",
263
456
  "json_data": kwargs, # httpx will JSON-encode this to a string
264
457
  }
265
458
 
266
459
 
267
- # Tool name -> Endpoint mapping
268
460
  TOOL_MAPPINGS: dict[str, EndpointMapping] = {
269
- # Phase 1: Core Building Tools
270
- "build_cell": EndpointMapping(
271
- method="GET",
272
- path="/api/build-cell",
273
- request_transformer=_build_cell_request,
274
- ),
275
461
  "build_cells": EndpointMapping(
276
462
  method="POST",
277
463
  path="/api/build-cells",
@@ -287,17 +473,11 @@ TOOL_MAPPINGS: dict[str, EndpointMapping] = {
287
473
  path="/api/cell-info",
288
474
  request_transformer=_get_cell_info_request,
289
475
  ),
290
- "download_gds": EndpointMapping(
291
- method="GET",
292
- path="/api/download/{path}.gds",
293
- request_transformer=_download_gds_request,
294
- response_transformer=_download_gds_response,
295
- ),
296
- # Phase 2: Verification Tools
297
476
  "check_drc": EndpointMapping(
298
477
  method="POST",
299
478
  path="/api/check-drc",
300
479
  request_transformer=_check_drc_request,
480
+ response_transformer=_check_drc_response,
301
481
  ),
302
482
  "check_connectivity": EndpointMapping(
303
483
  method="POST",
@@ -309,7 +489,6 @@ TOOL_MAPPINGS: dict[str, EndpointMapping] = {
309
489
  path="/api/check-lvs",
310
490
  request_transformer=_check_lvs_request,
311
491
  ),
312
- # Phase 4: Simulation & Advanced Tools
313
492
  "simulate_component": EndpointMapping(
314
493
  method="GET",
315
494
  path="/api/simulate",
@@ -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
 
@@ -15,7 +15,6 @@ __all__ = [
15
15
  ]
16
16
 
17
17
 
18
- # Define available resources
19
18
  RESOURCES: list[Resource] = [
20
19
  Resource(
21
20
  uri="instructions://build_custom_cell",
@@ -30,7 +29,6 @@ RESOURCES: list[Resource] = [
30
29
  ]
31
30
 
32
31
 
33
- # Resource content
34
32
  RESOURCE_CONTENT = {
35
33
  "instructions://build_custom_cell": """---
36
34
  Workflow: Creating Custom Components in GDSFactory+ Projects
@@ -71,7 +69,6 @@ def custom_component_name(
71
69
  Returns:
72
70
  Component description
73
71
  \"\"\"
74
- # Implementation using PDK functions
75
72
  c = pdk_function(parameters...)
76
73
  return c
77
74
 
mcp_standalone/server.py CHANGED
@@ -40,10 +40,8 @@ def create_server(api_url: str | None = None) -> Server:
40
40
  Returns:
41
41
  Configured MCP Server instance
42
42
  """
43
- # Create server instance
44
43
  server = Server("gdsfactoryplus")
45
44
 
46
- # Create HTTP client for FastAPI backend
47
45
  client = FastAPIClient(api_url)
48
46
 
49
47
  @server.list_tools()
@@ -83,7 +81,6 @@ def create_server(api_url: str | None = None) -> Server:
83
81
  """
84
82
  logger.info("Resource requested: %s", uri)
85
83
 
86
- # Get resource content
87
84
  content = get_resource_content(uri)
88
85
  if content is None:
89
86
  error_msg = f"Unknown resource URI: {uri}"
@@ -106,7 +103,6 @@ def create_server(api_url: str | None = None) -> Server:
106
103
  logger.info("Tool called: %s", name)
107
104
  logger.debug("Arguments: %s", arguments)
108
105
 
109
- # Handle special tools that don't require HTTP requests
110
106
  if name == "list_projects":
111
107
  try:
112
108
  projects = client.list_projects()
@@ -138,7 +134,6 @@ def create_server(api_url: str | None = None) -> Server:
138
134
  logger.exception(error_msg)
139
135
  return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
140
136
 
141
- # Get the endpoint mapping
142
137
  mapping = get_mapping(name)
143
138
  if mapping is None:
144
139
  error_msg = f"Unknown tool: {name}"
@@ -146,21 +141,17 @@ def create_server(api_url: str | None = None) -> Server:
146
141
  return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
147
142
 
148
143
  try:
149
- # Extract project parameter (optional)
150
144
  project = arguments.get("project")
151
145
 
152
- # Transform MCP arguments to HTTP request parameters
153
146
  transformed = transform_request(name, arguments)
154
147
  logger.debug("Transformed request: %s", transformed)
155
148
 
156
- # Extract request parameters
157
149
  method = mapping.method
158
150
  path = transformed.get("path", mapping.path)
159
151
  params = transformed.get("params")
160
152
  json_data = transformed.get("json_data")
161
153
  data = transformed.get("data")
162
154
 
163
- # Make HTTP request to FastAPI backend (with optional project routing)
164
155
  response = await client.request(
165
156
  method=method,
166
157
  path=path,
@@ -170,11 +161,9 @@ def create_server(api_url: str | None = None) -> Server:
170
161
  project=project,
171
162
  )
172
163
 
173
- # Transform response to MCP format
174
164
  result = transform_response(name, response)
175
165
  logger.debug("Tool result: %s", result)
176
166
 
177
- # Return as text content
178
167
  return [
179
168
  TextContent(
180
169
  type="text",
@@ -192,7 +181,6 @@ def create_server(api_url: str | None = None) -> Server:
192
181
  )
193
182
  ]
194
183
 
195
- # Store client reference for cleanup
196
184
  server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
197
185
 
198
186
  return server
@@ -215,10 +203,15 @@ def _log_server_discovery(client: FastAPIClient) -> None:
215
203
  logger.info(" ... and %d more", len(projects) - 3)
216
204
  else:
217
205
  logger.warning(
218
- "No GDSFactory+ servers found. Start a server with: "
219
- "gfp serve --port 8787"
206
+ "No GDSFactory+ servers found. To start a server:\n"
207
+ " 1. Open VSCode with the GDSFactory+ extension installed\n"
208
+ " 2. Open a GDSFactory+ project folder\n"
209
+ " 3. The extension will automatically start and register the server"
220
210
  )
221
211
  logger.info("Registry location: %s", get_registry_path())
212
+ logger.info(
213
+ "Alternatively, set GFP_API_URL environment variable to connect to a specific server"
214
+ )
222
215
  except Exception as e:
223
216
  logger.warning("Could not read server registry: %s", e)
224
217
  logger.info(
@@ -233,7 +226,6 @@ async def run_server(api_url: str | None = None) -> None:
233
226
  Args:
234
227
  api_url: Optional FastAPI base URL (default from config)
235
228
  """
236
- # Configure logging
237
229
  log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
238
230
  logging.basicConfig(
239
231
  level=log_level,
@@ -241,21 +233,18 @@ async def run_server(api_url: str | None = None) -> None:
241
233
  )
242
234
 
243
235
  logger.info("Starting GDSFactory+ MCP server")
244
- logger.info("FastAPI base URL: %s", MCPConfig.get_api_url(api_url))
236
+ base_url_info = MCPConfig.get_api_url(api_url) or "registry-based routing"
237
+ logger.info("Base URL: %s", base_url_info)
245
238
  logger.info("Timeout: %ds", MCPConfig.get_timeout())
246
239
 
247
- # Create server
248
240
  server = create_server(api_url)
249
241
  client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
250
242
 
251
243
  try:
252
- # Start HTTP client
253
244
  await client.start()
254
245
 
255
- # Discover available servers
256
246
  _log_server_discovery(client)
257
247
 
258
- # Run server with STDIO transport
259
248
  async with stdio_server() as (read_stream, write_stream):
260
249
  logger.info("MCP server ready (STDIO transport)")
261
250
  await server.run(
@@ -270,7 +259,6 @@ async def run_server(api_url: str | None = None) -> None:
270
259
  logger.exception("MCP server error")
271
260
  raise
272
261
  finally:
273
- # Cleanup
274
262
  await client.close()
275
263
  logger.info("MCP server stopped")
276
264
 
mcp_standalone/tools.py CHANGED
@@ -15,7 +15,6 @@ __all__ = [
15
15
  ]
16
16
 
17
17
 
18
- # Project discovery tools (for multi-project support)
19
18
  PROJECT_TOOLS: list[Tool] = [
20
19
  Tool(
21
20
  name="list_projects",
@@ -52,14 +51,13 @@ PROJECT_TOOLS: list[Tool] = [
52
51
  ),
53
52
  ]
54
53
 
55
- # Standard optional project parameter for all tools
56
54
  PROJECT_PARAM_SCHEMA = {
57
55
  "project": {
58
56
  "type": "string",
59
57
  "description": (
60
58
  "Optional project name or path to route this request to a specific "
61
- "server. If not provided, routes to the default server. "
62
- "Use list_projects to see available projects."
59
+ "server. If not provided, uses the first available server from the registry "
60
+ "or GFP_API_URL if set. Use list_projects to see available projects."
63
61
  ),
64
62
  },
65
63
  }
@@ -80,48 +78,13 @@ def _add_project_param(schema: dict) -> dict:
80
78
  return schema
81
79
 
82
80
 
83
- # Phase 1: Core Building Tools (5 tools)
84
81
  CORE_TOOLS: list[Tool] = [
85
- Tool(
86
- name="build_cell",
87
- description=(
88
- "Build a single GDS cell by name. This creates the physical layout "
89
- "file (.gds) for a photonic component. The build happens in the "
90
- "background and the GDS file will be saved to the project build "
91
- "directory."
92
- ),
93
- inputSchema=_add_project_param(
94
- {
95
- "type": "object",
96
- "properties": {
97
- "name": {
98
- "type": "string",
99
- "description": "Name of the cell/component to build",
100
- },
101
- "with_metadata": {
102
- "type": "boolean",
103
- "description": (
104
- "Include metadata in the GDS file (default: true)"
105
- ),
106
- "default": True,
107
- },
108
- "register": {
109
- "type": "boolean",
110
- "description": (
111
- "Re-register the cell in the KLayout cache (default: true)"
112
- ),
113
- "default": True,
114
- },
115
- },
116
- "required": ["name"],
117
- }
118
- ),
119
- ),
120
82
  Tool(
121
83
  name="build_cells",
122
84
  description=(
123
- "Build multiple GDS cells by name in one operation. This is more "
124
- "efficient than building cells one at a time. All cells are built "
85
+ "Build one or more GDS cells by name. This creates the physical layout "
86
+ "files (.gds) for photonic components. Pass a list of cell names to build. "
87
+ "For a single cell, pass a list with one element. All cells are built "
125
88
  "in the background and saved to the project build directory."
126
89
  ),
127
90
  inputSchema=_add_project_param(
@@ -131,7 +94,7 @@ CORE_TOOLS: list[Tool] = [
131
94
  "names": {
132
95
  "type": "array",
133
96
  "items": {"type": "string"},
134
- "description": "List of cell/component names to build",
97
+ "description": "List of cell/component names to build (can be a single-item list)",
135
98
  },
136
99
  "with_metadata": {
137
100
  "type": "boolean",
@@ -186,32 +149,8 @@ CORE_TOOLS: list[Tool] = [
186
149
  }
187
150
  ),
188
151
  ),
189
- Tool(
190
- name="download_gds",
191
- description=(
192
- "Download a GDS file from the project build directory. Returns the "
193
- "file path to the downloaded GDS file. The file must have been "
194
- "previously built using build_cell or build_cells."
195
- ),
196
- inputSchema=_add_project_param(
197
- {
198
- "type": "object",
199
- "properties": {
200
- "path": {
201
- "type": "string",
202
- "description": (
203
- "Relative path to the GDS file (without .gds extension). "
204
- "For example, 'mzi' or 'components/coupler'"
205
- ),
206
- },
207
- },
208
- "required": ["path"],
209
- }
210
- ),
211
- ),
212
152
  ]
213
153
 
214
- # Phase 2: Verification Tools
215
154
  VERIFICATION_TOOLS: list[Tool] = [
216
155
  Tool(
217
156
  name="check_drc",
@@ -328,10 +267,8 @@ VERIFICATION_TOOLS: list[Tool] = [
328
267
  ),
329
268
  ]
330
269
 
331
- # Phase 3: SPICE Workflow Tools (to be implemented)
332
270
  SPICE_TOOLS: list[Tool] = []
333
271
 
334
- # Phase 4: Simulation & Advanced Tools
335
272
  ADVANCED_TOOLS: list[Tool] = [
336
273
  Tool(
337
274
  name="simulate_component",
@@ -496,7 +433,6 @@ ADVANCED_TOOLS: list[Tool] = [
496
433
  ),
497
434
  ]
498
435
 
499
- # All tools (Phase 1 + Project tools)
500
436
  TOOLS: list[Tool] = [
501
437
  *PROJECT_TOOLS, # Project discovery tools (always available)
502
438
  *CORE_TOOLS,
@@ -1,14 +0,0 @@
1
- gfp_mcp-0.2.1.dist-info/licenses/LICENSE,sha256=ixSuHdKKXzNJw_eTgAxHzaCNIds8k48hytA_eJgA8gQ,225
2
- mcp_standalone/__init__.py,sha256=B6JB7U_vEMARO87RzNDesfSZdGgD1U-RC2GkL2ijOCA,1069
3
- mcp_standalone/client.py,sha256=vS_mw3frp5dQr2s_uFbPH-cF4k98rOJFGZLIz1FOU7A,8371
4
- mcp_standalone/config.py,sha256=1B00PLrKOz96c62lkhIgrraF-HWZ015uoPZ7okCcuKk,1525
5
- mcp_standalone/mappings.py,sha256=2J2DEZzXTdmi6VmrtCeI1cg-2T7tYHkytRGrZJ62zog,10583
6
- mcp_standalone/registry.py,sha256=1E61UalVot8HUS3cALjM7ejYB0qR6tI5QbQSZZeQe7Y,6401
7
- mcp_standalone/resources.py,sha256=iMkYIyTxLWwWE0NLxprLabGFYbPnUaIbgwwtbvZ2av0,3606
8
- mcp_standalone/server.py,sha256=jDCns5Cgb5JZahbx8pjCCDqryT-RiLCOFD5FIpgr3OA,9149
9
- mcp_standalone/tools.py,sha256=totbebwVzqOej1TjmY6lOZ7raSPFIwGWmWfOFVN3IyE,18734
10
- gfp_mcp-0.2.1.dist-info/METADATA,sha256=16kFH1_7TvI11tZnuXXrdTyQ6os5s7I5E7mTvSdRfVA,6858
11
- gfp_mcp-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- gfp_mcp-0.2.1.dist-info/entry_points.txt,sha256=mgyus9dsB_8mjgnztuHNPqzPi-7HcPg1iYzfM5NMIjk,61
13
- gfp_mcp-0.2.1.dist-info/top_level.txt,sha256=g2hRJHoDDPNtrNdXR70T7FR9Ev6DTRJiGW7ZvlvnXMc,15
14
- gfp_mcp-0.2.1.dist-info/RECORD,,