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.
- {mcp_standalone → gfp_mcp}/__init__.py +10 -8
- {mcp_standalone → gfp_mcp}/client.py +57 -33
- gfp_mcp/config.py +161 -0
- {mcp_standalone → gfp_mcp}/registry.py +0 -4
- gfp_mcp/render.py +139 -0
- {mcp_standalone → gfp_mcp}/resources.py +0 -3
- gfp_mcp/samples.py +206 -0
- gfp_mcp/server.py +235 -0
- gfp_mcp/tools/__init__.py +134 -0
- gfp_mcp/tools/base.py +235 -0
- gfp_mcp/tools/bbox.py +115 -0
- gfp_mcp/tools/build.py +159 -0
- gfp_mcp/tools/cells.py +103 -0
- gfp_mcp/tools/connectivity.py +70 -0
- gfp_mcp/tools/drc.py +379 -0
- gfp_mcp/tools/freeze.py +82 -0
- gfp_mcp/tools/lvs.py +86 -0
- gfp_mcp/tools/pdk.py +47 -0
- gfp_mcp/tools/port.py +82 -0
- gfp_mcp/tools/project.py +160 -0
- gfp_mcp/tools/samples.py +215 -0
- gfp_mcp/tools/simulation.py +153 -0
- gfp_mcp/utils.py +55 -0
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.3.2.dist-info}/METADATA +37 -8
- gfp_mcp-0.3.2.dist-info/RECORD +29 -0
- gfp_mcp-0.3.2.dist-info/entry_points.txt +2 -0
- gfp_mcp-0.3.2.dist-info/top_level.txt +1 -0
- gfp_mcp-0.2.1.dist-info/RECORD +0 -14
- gfp_mcp-0.2.1.dist-info/entry_points.txt +0 -2
- gfp_mcp-0.2.1.dist-info/top_level.txt +0 -1
- mcp_standalone/config.py +0 -56
- mcp_standalone/mappings.py +0 -386
- mcp_standalone/server.py +0 -294
- mcp_standalone/tools.py +0 -530
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.3.2.dist-info}/WHEEL +0 -0
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
"""MCP
|
|
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
|
-
-
|
|
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
|
|
15
|
+
from gfp_mcp import main
|
|
16
16
|
main()
|
|
17
17
|
|
|
18
18
|
Or via CLI:
|
|
19
|
-
gfp
|
|
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
|
|
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=
|
|
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(
|
|
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
|
|
102
|
+
ValueError: If base URL cannot be resolved
|
|
83
103
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
134
|
+
return default_url
|
|
96
135
|
|
|
97
|
-
|
|
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)
|
gfp_mcp/config.py
ADDED
|
@@ -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
|
|
gfp_mcp/render.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""GDS rendering utilities for MCP server.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for rendering GDS files to PNG images,
|
|
4
|
+
enabling automatic image return when building cells.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from mcp.types import ImageContent
|
|
14
|
+
|
|
15
|
+
from .registry import ServerRegistry
|
|
16
|
+
from .utils import wait_for_gds_file
|
|
17
|
+
|
|
18
|
+
__all__ = ["render_gds_to_png", "render_built_cells", "HAS_KLAYOUT"]
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import klayout.db as db
|
|
24
|
+
import klayout.lay as lay
|
|
25
|
+
|
|
26
|
+
HAS_KLAYOUT = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
HAS_KLAYOUT = False
|
|
29
|
+
db = None # type: ignore[assignment]
|
|
30
|
+
lay = None # type: ignore[assignment]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_gds_to_png(
|
|
34
|
+
gds_path: Path,
|
|
35
|
+
width: int = 800,
|
|
36
|
+
height: int = 600,
|
|
37
|
+
) -> bytes | None:
|
|
38
|
+
"""Render GDS file to PNG image using klayout.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
gds_path: Path to the GDS file to render
|
|
42
|
+
width: Image width in pixels (default: 800)
|
|
43
|
+
height: Image height in pixels (default: 600)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
PNG image as bytes, or None if klayout is not available or rendering fails
|
|
47
|
+
"""
|
|
48
|
+
if not HAS_KLAYOUT:
|
|
49
|
+
logger.debug("klayout not available, skipping GDS rendering")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
if not gds_path.exists():
|
|
53
|
+
logger.warning("GDS file not found for rendering: %s", gds_path)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
layout = db.Layout()
|
|
58
|
+
layout.read(str(gds_path))
|
|
59
|
+
|
|
60
|
+
layout_view = lay.LayoutView()
|
|
61
|
+
layout_view.load_layout(str(gds_path), True)
|
|
62
|
+
|
|
63
|
+
layout_view.max_hier()
|
|
64
|
+
|
|
65
|
+
layout_view.zoom_fit()
|
|
66
|
+
|
|
67
|
+
pixel_buffer = layout_view.get_pixels(width, height)
|
|
68
|
+
|
|
69
|
+
png_bytes = pixel_buffer.to_png_data()
|
|
70
|
+
|
|
71
|
+
logger.debug("Rendered GDS to PNG: %s (%dx%d)", gds_path, width, height)
|
|
72
|
+
return png_bytes
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning("Failed to render GDS to PNG: %s - %s", gds_path, e)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def render_built_cells(
|
|
80
|
+
cell_names: list[str],
|
|
81
|
+
project: str | None,
|
|
82
|
+
) -> list[ImageContent]:
|
|
83
|
+
"""Render built GDS cells to PNG images.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
cell_names: List of cell names to render
|
|
87
|
+
project: Optional project name for routing
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of ImageContent for successfully rendered cells
|
|
91
|
+
"""
|
|
92
|
+
images: list[ImageContent] = []
|
|
93
|
+
|
|
94
|
+
if not cell_names:
|
|
95
|
+
return images
|
|
96
|
+
|
|
97
|
+
if not HAS_KLAYOUT:
|
|
98
|
+
logger.debug("klayout not available, skipping cell rendering")
|
|
99
|
+
return images
|
|
100
|
+
|
|
101
|
+
registry = ServerRegistry()
|
|
102
|
+
server_info = None
|
|
103
|
+
|
|
104
|
+
if project:
|
|
105
|
+
server_info = registry.get_server_by_project(project)
|
|
106
|
+
if not server_info:
|
|
107
|
+
servers = registry.list_servers()
|
|
108
|
+
if servers:
|
|
109
|
+
server_info = servers[0]
|
|
110
|
+
|
|
111
|
+
if not server_info:
|
|
112
|
+
logger.debug(
|
|
113
|
+
"No server found in registry, cannot determine project path for GDS rendering"
|
|
114
|
+
)
|
|
115
|
+
return images
|
|
116
|
+
|
|
117
|
+
project_path = Path(server_info.project_path)
|
|
118
|
+
gds_dir = project_path / "build" / "gds"
|
|
119
|
+
|
|
120
|
+
for cell_name in cell_names:
|
|
121
|
+
gds_path = gds_dir / f"{cell_name}.gds"
|
|
122
|
+
|
|
123
|
+
if await wait_for_gds_file(gds_path):
|
|
124
|
+
png_bytes = render_gds_to_png(gds_path)
|
|
125
|
+
if png_bytes:
|
|
126
|
+
images.append(
|
|
127
|
+
ImageContent(
|
|
128
|
+
type="image",
|
|
129
|
+
data=base64.b64encode(png_bytes).decode("utf-8"),
|
|
130
|
+
mimeType="image/png",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
logger.info("Added PNG image for cell: %s", cell_name)
|
|
134
|
+
else:
|
|
135
|
+
logger.debug("Failed to render GDS to PNG for cell: %s", cell_name)
|
|
136
|
+
else:
|
|
137
|
+
logger.debug("GDS file not found within timeout for cell: %s", cell_name)
|
|
138
|
+
|
|
139
|
+
return images
|
|
@@ -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
|
|