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.
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.2.4.dist-info}/METADATA +25 -8
- gfp_mcp-0.2.4.dist-info/RECORD +14 -0
- mcp_standalone/__init__.py +1 -1
- mcp_standalone/client.py +57 -19
- mcp_standalone/config.py +3 -9
- mcp_standalone/mappings.py +246 -67
- mcp_standalone/registry.py +0 -4
- mcp_standalone/resources.py +0 -3
- mcp_standalone/server.py +9 -21
- mcp_standalone/tools.py +6 -70
- gfp_mcp-0.2.1.dist-info/RECORD +0 -14
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.2.4.dist-info}/WHEEL +0 -0
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.2.4.dist-info}/entry_points.txt +0 -0
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {gfp_mcp-0.2.1.dist-info → gfp_mcp-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gfp-mcp
|
|
3
|
-
Version: 0.2.
|
|
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
|
[](https://pypi.org/project/gfp-mcp/)
|
|
41
43
|
[](https://pypi.org/project/gfp-mcp/)
|
|
42
|
-
[](https://github.com/doplaydo/gfp-mcp/actions)
|
|
43
45
|
[](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
|
-
|
|
67
|
+
uv tool install gfp-mcp
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
|
|
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
|
-
- **
|
|
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,,
|
mcp_standalone/__init__.py
CHANGED
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=
|
|
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
|
|
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
|
-
|
|
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
|
|
mcp_standalone/mappings.py
CHANGED
|
@@ -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
|
|
109
|
-
"""Transform
|
|
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
|
|
98
|
+
Dict with 'json_data' key for request body
|
|
116
99
|
"""
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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 (
|
|
123
|
+
response: FastAPI response (XML string or dict with 'content' key)
|
|
128
124
|
|
|
129
125
|
Returns:
|
|
130
|
-
|
|
126
|
+
Structured summary with all violations including location data,
|
|
127
|
+
or error dict if parsing fails
|
|
131
128
|
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
252
|
+
values_elem: XML element containing polygon coordinates
|
|
143
253
|
|
|
144
254
|
Returns:
|
|
145
|
-
Dict with
|
|
255
|
+
Dict with bbox, centroid, and size, or None if parsing fails
|
|
146
256
|
"""
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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",
|
mcp_standalone/registry.py
CHANGED
|
@@ -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
|
|
mcp_standalone/resources.py
CHANGED
|
@@ -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.
|
|
219
|
-
"
|
|
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
|
-
|
|
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,
|
|
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
|
|
124
|
-
"
|
|
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,
|
gfp_mcp-0.2.1.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|