gfp-mcp 0.2.1__tar.gz → 0.3.2__tar.gz

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.
Files changed (56) hide show
  1. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/PKG-INFO +37 -8
  2. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/README.md +31 -7
  3. {gfp_mcp-0.2.1/mcp_standalone → gfp_mcp-0.3.2/gfp_mcp}/__init__.py +10 -8
  4. {gfp_mcp-0.2.1/mcp_standalone → gfp_mcp-0.3.2/gfp_mcp}/client.py +57 -33
  5. gfp_mcp-0.3.2/gfp_mcp/config.py +161 -0
  6. {gfp_mcp-0.2.1/mcp_standalone → gfp_mcp-0.3.2/gfp_mcp}/registry.py +0 -4
  7. gfp_mcp-0.3.2/gfp_mcp/render.py +139 -0
  8. {gfp_mcp-0.2.1/mcp_standalone → gfp_mcp-0.3.2/gfp_mcp}/resources.py +0 -3
  9. gfp_mcp-0.3.2/gfp_mcp/samples.py +206 -0
  10. gfp_mcp-0.3.2/gfp_mcp/server.py +235 -0
  11. gfp_mcp-0.3.2/gfp_mcp/tools/__init__.py +134 -0
  12. gfp_mcp-0.3.2/gfp_mcp/tools/base.py +235 -0
  13. gfp_mcp-0.3.2/gfp_mcp/tools/bbox.py +115 -0
  14. gfp_mcp-0.3.2/gfp_mcp/tools/build.py +159 -0
  15. gfp_mcp-0.3.2/gfp_mcp/tools/cells.py +103 -0
  16. gfp_mcp-0.3.2/gfp_mcp/tools/connectivity.py +70 -0
  17. gfp_mcp-0.3.2/gfp_mcp/tools/drc.py +379 -0
  18. gfp_mcp-0.3.2/gfp_mcp/tools/freeze.py +82 -0
  19. gfp_mcp-0.3.2/gfp_mcp/tools/lvs.py +86 -0
  20. gfp_mcp-0.3.2/gfp_mcp/tools/pdk.py +47 -0
  21. gfp_mcp-0.3.2/gfp_mcp/tools/port.py +82 -0
  22. gfp_mcp-0.3.2/gfp_mcp/tools/project.py +160 -0
  23. gfp_mcp-0.3.2/gfp_mcp/tools/samples.py +215 -0
  24. gfp_mcp-0.3.2/gfp_mcp/tools/simulation.py +153 -0
  25. gfp_mcp-0.3.2/gfp_mcp/utils.py +55 -0
  26. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/gfp_mcp.egg-info/PKG-INFO +37 -8
  27. gfp_mcp-0.3.2/gfp_mcp.egg-info/SOURCES.txt +45 -0
  28. gfp_mcp-0.3.2/gfp_mcp.egg-info/entry_points.txt +2 -0
  29. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/gfp_mcp.egg-info/requires.txt +6 -0
  30. gfp_mcp-0.3.2/gfp_mcp.egg-info/top_level.txt +1 -0
  31. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/pyproject.toml +9 -3
  32. gfp_mcp-0.3.2/tests/test_gfp_phase4.py +308 -0
  33. gfp_mcp-0.3.2/tests/test_gfp_server.py +331 -0
  34. gfp_mcp-0.3.2/tests/test_gfp_tool_handlers.py +602 -0
  35. gfp_mcp-0.3.2/tests/test_gfp_tools_base.py +313 -0
  36. gfp_mcp-0.3.2/tests/test_gfp_tools_init.py +170 -0
  37. gfp_mcp-0.3.2/tests/test_gfp_utils.py +83 -0
  38. gfp_mcp-0.3.2/tests/test_mcp_config.py +208 -0
  39. gfp_mcp-0.3.2/tests/test_mcp_handlers.py +767 -0
  40. gfp_mcp-0.3.2/tests/test_mcp_integration.py +323 -0
  41. gfp_mcp-0.3.2/tests/test_mcp_render.py +299 -0
  42. gfp_mcp-0.3.2/tests/test_mcp_samples.py +491 -0
  43. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/tests/test_mcp_tools.py +76 -51
  44. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/tests/test_registry.py +1 -13
  45. gfp_mcp-0.2.1/gfp_mcp.egg-info/SOURCES.txt +0 -21
  46. gfp_mcp-0.2.1/gfp_mcp.egg-info/entry_points.txt +0 -2
  47. gfp_mcp-0.2.1/gfp_mcp.egg-info/top_level.txt +0 -1
  48. gfp_mcp-0.2.1/mcp_standalone/config.py +0 -56
  49. gfp_mcp-0.2.1/mcp_standalone/mappings.py +0 -386
  50. gfp_mcp-0.2.1/mcp_standalone/server.py +0 -294
  51. gfp_mcp-0.2.1/mcp_standalone/tools.py +0 -530
  52. gfp_mcp-0.2.1/tests/test_mcp_integration.py +0 -267
  53. gfp_mcp-0.2.1/tests/test_mcp_mappings.py +0 -320
  54. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/LICENSE +0 -0
  55. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/gfp_mcp.egg-info/dependency_links.txt +0 -0
  56. {gfp_mcp-0.2.1 → gfp_mcp-0.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gfp-mcp
3
- Version: 0.2.1
3
+ Version: 0.3.2
4
4
  Summary: Model Context Protocol (MCP) server for GDSFactory+ photonic IC design
5
5
  Author: GDSFactory+ Team
6
6
  License: MIT
@@ -27,19 +27,24 @@ License-File: LICENSE
27
27
  Requires-Dist: mcp>=1.7.1
28
28
  Requires-Dist: httpx>=0.25.0
29
29
  Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
30
+ Requires-Dist: tomli>=2.0.0; python_version < "3.11"
30
31
  Requires-Dist: psutil>=5.9.0
32
+ Provides-Extra: render
33
+ Requires-Dist: klayout>=0.28.0; extra == "render"
31
34
  Provides-Extra: dev
32
35
  Requires-Dist: pytest>=7.0.0; extra == "dev"
33
36
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
37
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
35
38
  Requires-Dist: ruff>=0.1.0; extra == "dev"
39
+ Requires-Dist: bump-my-version>=0.26.0; extra == "dev"
40
+ Requires-Dist: build>=1.4.0; extra == "dev"
36
41
  Dynamic: license-file
37
42
 
38
43
  # GDSFactory+ MCP Server
39
44
 
40
45
  [![PyPI version](https://img.shields.io/pypi/v/gfp-mcp.svg)](https://pypi.org/project/gfp-mcp/)
41
46
  [![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)
47
+ [![Tests](https://github.com/doplaydo/gfp-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/doplaydo/gfp-mcp/actions)
43
48
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
44
49
 
45
50
  Model Context Protocol (MCP) server for GDSFactory+ that enables AI assistants like Claude to design and build photonic integrated circuits.
@@ -57,15 +62,32 @@ This MCP server connects AI assistants to [GDSFactory+](https://gdsfactory.com),
57
62
 
58
63
  ### 2. Install the MCP Server
59
64
 
65
+ **With uv (recommended):**
66
+
67
+ If you don't have `uv` installed, see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
68
+
60
69
  ```bash
61
- pip install gfp-mcp
70
+ uv tool install gfp-mcp
62
71
  ```
63
72
 
64
- Or with uv:
73
+ <details>
74
+ <summary>Ephemeral approach</summary>
75
+
76
+ Run without installing:
65
77
 
66
78
  ```bash
67
79
  uvx --from gfp-mcp gfp-mcp-serve
68
80
  ```
81
+ </details>
82
+
83
+ <details>
84
+ <summary><strong>Alternative: pip install</strong></summary>
85
+
86
+ ```bash
87
+ pip install gfp-mcp
88
+ ```
89
+
90
+ </details>
69
91
 
70
92
  ### 3. Connect to Your AI Assistant
71
93
 
@@ -143,16 +165,23 @@ Try these commands with your AI assistant:
143
165
 
144
166
  ## Available Tools
145
167
 
146
- - **build_cell** - Build a single GDS cell by name
147
- - **build_cells** - Build multiple GDS cells in batch
168
+ - **build_cells** - Build one or more GDS cells by name (pass a list, can be single-item)
148
169
  - **list_cells** - List all available photonic components
149
170
  - **get_cell_info** - Get detailed component metadata
150
- - **download_gds** - Download built GDS files
151
171
  - **list_projects** - List all running GDSFactory+ server instances
152
172
  - **get_project_info** - Get detailed information about a specific project
153
- - **check_drc** - Run Design Rule Check verification
173
+ - **check_drc** - Run Design Rule Check verification (returns structured format with all violations including simplified location data for LLM-friendly troubleshooting)
154
174
  - **check_connectivity** - Run connectivity verification
155
175
  - **check_lvs** - Run Layout vs. Schematic verification
176
+ - **simulate_component** - Run SAX circuit simulations with custom parameters
177
+ - Basic: `{"name": "mzi"}` - Simulate with default parameters
178
+ - Custom layout: `{"name": "mzi", "layout": {"length_mmi": 12, "gap_mmi": 0.3}}` - Customize component geometry
179
+ - Wavelength sweep: `{"name": "coupler", "model": {"wl": [1.5, 1.55, 1.6]}}` - Simulate at multiple wavelengths
180
+ - Full example: `{"name": "mzi", "layout": {"length": 100}, "model": {"wl": [1.5, 1.55, 1.6], "loss": 0.2}, "how": "from_layout"}`
181
+ - **get_port_center** - Get physical coordinates of component ports
182
+ - **generate_bbox** - Generate bounding box GDS from layout
183
+ - **freeze_cell** - Freeze parametric cell as static netlist
184
+ - **get_pdk_info** - Get current PDK information
156
185
 
157
186
  ## Multi-Project Support
158
187
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI version](https://img.shields.io/pypi/v/gfp-mcp.svg)](https://pypi.org/project/gfp-mcp/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/gfp-mcp.svg)](https://pypi.org/project/gfp-mcp/)
5
- [![Tests](https://github.com/doplaydo/gfp-mcp/workflows/Tests/badge.svg)](https://github.com/doplaydo/gfp-mcp/actions)
5
+ [![Tests](https://github.com/doplaydo/gfp-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/doplaydo/gfp-mcp/actions)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  Model Context Protocol (MCP) server for GDSFactory+ that enables AI assistants like Claude to design and build photonic integrated circuits.
@@ -20,15 +20,32 @@ This MCP server connects AI assistants to [GDSFactory+](https://gdsfactory.com),
20
20
 
21
21
  ### 2. Install the MCP Server
22
22
 
23
+ **With uv (recommended):**
24
+
25
+ If you don't have `uv` installed, see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
26
+
23
27
  ```bash
24
- pip install gfp-mcp
28
+ uv tool install gfp-mcp
25
29
  ```
26
30
 
27
- Or with uv:
31
+ <details>
32
+ <summary>Ephemeral approach</summary>
33
+
34
+ Run without installing:
28
35
 
29
36
  ```bash
30
37
  uvx --from gfp-mcp gfp-mcp-serve
31
38
  ```
39
+ </details>
40
+
41
+ <details>
42
+ <summary><strong>Alternative: pip install</strong></summary>
43
+
44
+ ```bash
45
+ pip install gfp-mcp
46
+ ```
47
+
48
+ </details>
32
49
 
33
50
  ### 3. Connect to Your AI Assistant
34
51
 
@@ -106,16 +123,23 @@ Try these commands with your AI assistant:
106
123
 
107
124
  ## Available Tools
108
125
 
109
- - **build_cell** - Build a single GDS cell by name
110
- - **build_cells** - Build multiple GDS cells in batch
126
+ - **build_cells** - Build one or more GDS cells by name (pass a list, can be single-item)
111
127
  - **list_cells** - List all available photonic components
112
128
  - **get_cell_info** - Get detailed component metadata
113
- - **download_gds** - Download built GDS files
114
129
  - **list_projects** - List all running GDSFactory+ server instances
115
130
  - **get_project_info** - Get detailed information about a specific project
116
- - **check_drc** - Run Design Rule Check verification
131
+ - **check_drc** - Run Design Rule Check verification (returns structured format with all violations including simplified location data for LLM-friendly troubleshooting)
117
132
  - **check_connectivity** - Run connectivity verification
118
133
  - **check_lvs** - Run Layout vs. Schematic verification
134
+ - **simulate_component** - Run SAX circuit simulations with custom parameters
135
+ - Basic: `{"name": "mzi"}` - Simulate with default parameters
136
+ - Custom layout: `{"name": "mzi", "layout": {"length_mmi": 12, "gap_mmi": 0.3}}` - Customize component geometry
137
+ - Wavelength sweep: `{"name": "coupler", "model": {"wl": [1.5, 1.55, 1.6]}}` - Simulate at multiple wavelengths
138
+ - Full example: `{"name": "mzi", "layout": {"length": 100}, "model": {"wl": [1.5, 1.55, 1.6], "loss": 0.2}, "how": "from_layout"}`
139
+ - **get_port_center** - Get physical coordinates of component ports
140
+ - **generate_bbox** - Generate bounding box GDS from layout
141
+ - **freeze_cell** - Freeze parametric cell as static netlist
142
+ - **get_pdk_info** - Get current PDK information
119
143
 
120
144
  ## Multi-Project Support
121
145
 
@@ -1,22 +1,22 @@
1
- """MCP Standalone Server for GDSFactory+.
1
+ """GDSFactory+ MCP Server.
2
2
 
3
3
  This package provides a Model Context Protocol (MCP) server that exposes
4
4
  GDSFactory+ operations as tools for AI assistants. The server uses STDIO
5
5
  transport and proxies requests to the FastAPI backend.
6
6
 
7
7
  Architecture:
8
- - Standalone MCP server (this package)
8
+ - Tool handlers in gfp_mcp/tools/ with co-located definitions and transformers
9
9
  - STDIO transport for universal compatibility
10
- - HTTP proxy to FastAPI backend
10
+ - HTTP proxy to FastAPI backend via client.py
11
+ - Multi-project routing via server registry
11
12
  - Zero changes to existing FastAPI server
12
- - No database conflicts (only FastAPI touches SQLite)
13
13
 
14
14
  Usage:
15
- from gdsfactoryplus.mcp_standalone import main
15
+ from gfp_mcp import main
16
16
  main()
17
17
 
18
18
  Or via CLI:
19
- gfp mcp-serve
19
+ gfp-mcp-serve
20
20
  """
21
21
 
22
22
  from __future__ import annotations
@@ -25,18 +25,20 @@ from .client import FastAPIClient
25
25
  from .config import MCPConfig
26
26
  from .resources import get_all_resources, get_resource_content
27
27
  from .server import create_server, main, run_server
28
- from .tools import get_all_tools, get_tool_by_name
28
+ from .tools import get_all_tools, get_handler, get_tool_by_name
29
29
 
30
30
  __all__ = [
31
+ "__version__",
31
32
  "FastAPIClient",
32
33
  "MCPConfig",
33
34
  "create_server",
34
35
  "main",
35
36
  "run_server",
36
37
  "get_all_tools",
38
+ "get_handler",
37
39
  "get_tool_by_name",
38
40
  "get_all_resources",
39
41
  "get_resource_content",
40
42
  ]
41
43
 
42
- __version__ = "0.2.1"
44
+ __version__ = "0.3.2"
@@ -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
 
@@ -256,17 +294,3 @@ class FastAPIClient:
256
294
  }
257
295
  for server in servers
258
296
  ]
259
-
260
- async def get_project_info(self, project: str) -> dict[str, Any]:
261
- """Get detailed information about a specific project.
262
-
263
- Args:
264
- project: Project name or path
265
-
266
- Returns:
267
- Project information from the server's /info endpoint
268
-
269
- Raises:
270
- ValueError: If project not found
271
- """
272
- return await self.request("GET", "/info", project=project)
@@ -0,0 +1,161 @@
1
+ """Configuration management for MCP standalone server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Final
8
+
9
+ try:
10
+ import tomllib # Python 3.11+
11
+ except ImportError:
12
+ import tomli as tomllib # type: ignore[import-not-found]
13
+
14
+ __all__ = ["MCPConfig", "get_gfp_api_key", "set_gfp_api_key"]
15
+
16
+
17
+ class MCPConfig:
18
+ """Configuration for MCP standalone server.
19
+
20
+ Manages environment variables and default settings for the MCP server
21
+ that proxies requests to the FastAPI backend.
22
+ """
23
+
24
+ API_URL: Final[str | None] = os.getenv("GFP_API_URL")
25
+
26
+ TIMEOUT: Final[int] = int(os.getenv("GFP_MCP_TIMEOUT", "300"))
27
+
28
+ DEBUG: Final[bool] = os.getenv("GFP_MCP_DEBUG", "false").lower() in (
29
+ "true",
30
+ "1",
31
+ "yes",
32
+ )
33
+
34
+ REGISTRY_API_URL: Final[str] = "https://registry.gdsfactory.com"
35
+
36
+ MAX_RETRIES: Final[int] = 3
37
+ RETRY_BACKOFF: Final[float] = 0.5 # Initial backoff in seconds
38
+
39
+ @classmethod
40
+ def get_api_url(cls, override: str | None = None) -> str | None:
41
+ """Get the FastAPI base URL.
42
+
43
+ Args:
44
+ override: Optional URL to override the environment variable
45
+
46
+ Returns:
47
+ The API base URL or None if not configured
48
+ """
49
+ return override or cls.API_URL
50
+
51
+ @classmethod
52
+ def get_timeout(cls) -> int:
53
+ """Get the timeout for tool calls.
54
+
55
+ Returns:
56
+ Timeout in seconds
57
+ """
58
+ return cls.TIMEOUT
59
+
60
+
61
+ def _read_api_key_from_toml(file_path: Path) -> str | None:
62
+ """Read the API key from a TOML configuration file.
63
+
64
+ Args:
65
+ file_path: Path to the TOML file.
66
+
67
+ Returns:
68
+ The API key string, or None if not found or file doesn't exist.
69
+ """
70
+ if not file_path.exists():
71
+ return None
72
+
73
+ try:
74
+ with open(file_path, "rb") as f:
75
+ config = tomllib.load(f)
76
+
77
+ return (
78
+ config.get("tool", {}).get("gdsfactoryplus", {}).get("api", {}).get("key")
79
+ )
80
+ except Exception:
81
+ return None
82
+
83
+
84
+ def get_gfp_api_key() -> str | None:
85
+ """Retrieve the GFP API key from environment variables or config files.
86
+
87
+ Checks sources in priority order:
88
+ 1. GFP_API_KEY environment variable
89
+ 2. ~/.gdsfactory/gdsfactoryplus.toml
90
+ 3. ./pyproject.toml
91
+
92
+ Returns:
93
+ The API key string, or None if not found.
94
+ """
95
+ # 1. Check environment variable (highest priority)
96
+ api_key = os.environ.get("GFP_API_KEY")
97
+ if api_key:
98
+ return api_key
99
+
100
+ # 2. Check global config file: ~/.gdsfactory/gdsfactoryplus.toml
101
+ global_config_path = Path.home() / ".gdsfactory" / "gdsfactoryplus.toml"
102
+ api_key = _read_api_key_from_toml(global_config_path)
103
+ if api_key:
104
+ return api_key
105
+
106
+ # 3. Check local project config: ./pyproject.toml
107
+ local_config_path = Path.cwd() / "pyproject.toml"
108
+ api_key = _read_api_key_from_toml(local_config_path)
109
+ if api_key:
110
+ return api_key
111
+
112
+ return None
113
+
114
+
115
+ def set_gfp_api_key(api_key: str) -> None:
116
+ """Save the GFP API key to the global config file.
117
+
118
+ Writes to: ~/.gdsfactory/gdsfactoryplus.toml
119
+
120
+ Args:
121
+ api_key: The API key to save.
122
+
123
+ Raises:
124
+ ValueError: If api_key is empty or None.
125
+ """
126
+ if not api_key:
127
+ raise ValueError("API key is required")
128
+
129
+ config_dir = Path.home() / ".gdsfactory"
130
+ config_path = config_dir / "gdsfactoryplus.toml"
131
+
132
+ # Create directory if it doesn't exist
133
+ config_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ # Load existing config or create new one
136
+ config: dict = {}
137
+ if config_path.exists():
138
+ with open(config_path, "rb") as f:
139
+ config = tomllib.load(f)
140
+
141
+ # Set the API key in the nested structure
142
+ if "tool" not in config:
143
+ config["tool"] = {}
144
+ if "gdsfactoryplus" not in config["tool"]:
145
+ config["tool"]["gdsfactoryplus"] = {}
146
+ if "api" not in config["tool"]["gdsfactoryplus"]:
147
+ config["tool"]["gdsfactoryplus"]["api"] = {}
148
+
149
+ config["tool"]["gdsfactoryplus"]["api"]["key"] = api_key
150
+
151
+ # Write back to file using tomli_w if available, otherwise manual formatting
152
+ try:
153
+ import tomli_w
154
+
155
+ with open(config_path, "wb") as f:
156
+ tomli_w.dump(config, f)
157
+ except ImportError:
158
+ # Fallback: write TOML manually for this simple structure
159
+ with open(config_path, "w") as f:
160
+ f.write("[tool.gdsfactoryplus.api]\n")
161
+ f.write(f'key = "{api_key}"\n')
@@ -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