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
gfp_mcp/samples.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Core module for downloading and extracting GDSFactory+ sample projects.
|
|
2
|
+
|
|
3
|
+
Provides async functions to download sample project ZIPs from the registry
|
|
4
|
+
and extract sample files (Python, YAML) for use by AI assistants.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import io
|
|
11
|
+
import logging
|
|
12
|
+
import mimetypes
|
|
13
|
+
import zipfile
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from .config import MCPConfig
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"GENERAL_PDK_PROJECTS",
|
|
22
|
+
"SAMPLE_EXTENSIONS",
|
|
23
|
+
"SampleFile",
|
|
24
|
+
"SampleProject",
|
|
25
|
+
"get_sample_file_content",
|
|
26
|
+
"list_sample_projects",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
GENERAL_PDK_PROJECTS = [
|
|
32
|
+
"photonics--basic--public--pdk",
|
|
33
|
+
"photonics--full--public--pdk",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
SAMPLE_EXTENSIONS = {".py", ".yml", ".yaml"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SampleProject:
|
|
41
|
+
"""A sample project with its list of sample file paths."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
files: list[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SampleFile:
|
|
49
|
+
"""A single sample file with its content."""
|
|
50
|
+
|
|
51
|
+
project: str
|
|
52
|
+
path: str
|
|
53
|
+
content: str
|
|
54
|
+
mime_type: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _download_project_zip(api_key: str, project_name: str) -> bytes:
|
|
58
|
+
"""Download a project ZIP from the registry.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
api_key: GFP API key for authentication.
|
|
62
|
+
project_name: Name of the project to download.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Raw ZIP file bytes.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
httpx.HTTPStatusError: If the request fails.
|
|
69
|
+
"""
|
|
70
|
+
url = f"{MCPConfig.REGISTRY_API_URL}/project/download/{project_name}"
|
|
71
|
+
headers = {"X-API-Key": api_key}
|
|
72
|
+
|
|
73
|
+
async with httpx.AsyncClient() as client:
|
|
74
|
+
response = await client.get(url, headers=headers, timeout=300.0)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
return response.content
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_sample_files(zip_data: bytes, project_name: str) -> list[str]:
|
|
80
|
+
"""Extract sample file paths from a project ZIP.
|
|
81
|
+
|
|
82
|
+
Finds files under any ``samples/`` directory, filters by extension,
|
|
83
|
+
and excludes ``__init__.py`` and ``__pycache__`` entries.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
zip_data: Raw ZIP file bytes.
|
|
87
|
+
project_name: Project name (unused, kept for future use).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Sorted list of file paths within the ZIP that are sample files.
|
|
91
|
+
"""
|
|
92
|
+
sample_files = []
|
|
93
|
+
|
|
94
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
95
|
+
for name in zf.namelist():
|
|
96
|
+
if zf.getinfo(name).is_dir():
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Must be under a samples/ directory
|
|
100
|
+
parts = name.split("/")
|
|
101
|
+
if "samples" not in parts:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Filter by extension
|
|
105
|
+
ext = "." + name.rsplit(".", 1)[-1] if "." in name else ""
|
|
106
|
+
if ext not in SAMPLE_EXTENSIONS:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Exclude __init__.py and __pycache__
|
|
110
|
+
basename = parts[-1]
|
|
111
|
+
if basename == "__init__.py" or "__pycache__" in parts:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
sample_files.append(name)
|
|
115
|
+
|
|
116
|
+
return sorted(sample_files)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _extract_file_content(zip_data: bytes, file_path: str) -> str:
|
|
120
|
+
"""Extract a single file's content from a ZIP.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
zip_data: Raw ZIP file bytes.
|
|
124
|
+
file_path: Path within the ZIP to extract.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
UTF-8 decoded file content.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
KeyError: If the file is not found in the ZIP.
|
|
131
|
+
"""
|
|
132
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
133
|
+
return zf.read(file_path).decode("utf-8")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_mime_type(path: str) -> str:
|
|
137
|
+
"""Determine MIME type from file path.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
path: File path to check.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
MIME type string, defaults to ``text/plain``.
|
|
144
|
+
"""
|
|
145
|
+
mime_type, _ = mimetypes.guess_type(path)
|
|
146
|
+
return mime_type or "text/plain"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def list_sample_projects(api_key: str) -> list[SampleProject]:
|
|
150
|
+
"""Download both General PDK ZIPs and list their sample files.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
api_key: GFP API key for authentication.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of SampleProject instances with file listings.
|
|
157
|
+
"""
|
|
158
|
+
tasks = [
|
|
159
|
+
_download_project_zip(api_key, project) for project in GENERAL_PDK_PROJECTS
|
|
160
|
+
]
|
|
161
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
162
|
+
|
|
163
|
+
projects = []
|
|
164
|
+
for project_name, result in zip(GENERAL_PDK_PROJECTS, results):
|
|
165
|
+
if isinstance(result, Exception):
|
|
166
|
+
logger.warning("Failed to download project %s: %s", project_name, result)
|
|
167
|
+
projects.append(SampleProject(name=project_name, files=[]))
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
files = _extract_sample_files(result, project_name)
|
|
171
|
+
projects.append(SampleProject(name=project_name, files=files))
|
|
172
|
+
|
|
173
|
+
return projects
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def get_sample_file_content(api_key: str, project: str, path: str) -> SampleFile:
|
|
177
|
+
"""Download a project ZIP and extract a specific file's content.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
api_key: GFP API key for authentication.
|
|
181
|
+
project: Project name (must be in GENERAL_PDK_PROJECTS).
|
|
182
|
+
path: File path within the ZIP.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
SampleFile with the file's content and metadata.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ValueError: If the project name is not recognized.
|
|
189
|
+
KeyError: If the file is not found in the ZIP.
|
|
190
|
+
"""
|
|
191
|
+
if project not in GENERAL_PDK_PROJECTS:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Unknown sample project: {project}. "
|
|
194
|
+
f"Available projects: {GENERAL_PDK_PROJECTS}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
zip_data = await _download_project_zip(api_key, project)
|
|
198
|
+
content = _extract_file_content(zip_data, path)
|
|
199
|
+
mime_type = _get_mime_type(path)
|
|
200
|
+
|
|
201
|
+
return SampleFile(
|
|
202
|
+
project=project,
|
|
203
|
+
path=path,
|
|
204
|
+
content=content,
|
|
205
|
+
mime_type=mime_type,
|
|
206
|
+
)
|
gfp_mcp/server.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""MCP server implementation for GDSFactory+.
|
|
2
|
+
|
|
3
|
+
This module provides the main MCP server that exposes GDSFactory+ operations
|
|
4
|
+
as tools for AI assistants using the STDIO transport.
|
|
5
|
+
|
|
6
|
+
The server uses a handler-based architecture where each tool has its own
|
|
7
|
+
handler class that encapsulates:
|
|
8
|
+
- Tool definition (name, description, input schema)
|
|
9
|
+
- Request transformation (MCP args -> HTTP params/body)
|
|
10
|
+
- Response transformation (HTTP response -> MCP format)
|
|
11
|
+
- Execution logic (handle method)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mcp.server import Server
|
|
21
|
+
from mcp.server.stdio import stdio_server
|
|
22
|
+
from mcp.types import (
|
|
23
|
+
ImageContent,
|
|
24
|
+
Resource,
|
|
25
|
+
TextContent,
|
|
26
|
+
Tool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .client import FastAPIClient
|
|
30
|
+
from .config import MCPConfig
|
|
31
|
+
from .resources import get_all_resources, get_resource_content
|
|
32
|
+
from .tools import get_all_tools, get_handler
|
|
33
|
+
|
|
34
|
+
__all__ = ["create_server", "run_server", "main"]
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_INSTRUCTIONS = """
|
|
40
|
+
GDSFactory+ MCP Server Instructions
|
|
41
|
+
===================================
|
|
42
|
+
|
|
43
|
+
This MCP server connects AI assistants to GDSFactory+ for photonic IC design.
|
|
44
|
+
Use this context to understand how to interact with the server effectively.
|
|
45
|
+
|
|
46
|
+
Server Discovery
|
|
47
|
+
----------------
|
|
48
|
+
Use the `list_projects` tool to discover running GDSFactory+ servers / projects.
|
|
49
|
+
Each project has its own server with a unique port.
|
|
50
|
+
|
|
51
|
+
If no servers are found, instruct the user to:
|
|
52
|
+
1. Open VSCode with the GDSFactory+ extension installed
|
|
53
|
+
2. Open a GDSFactory+ project folder
|
|
54
|
+
3. The extension will automatically start and register the server
|
|
55
|
+
|
|
56
|
+
Typically, users will invoke you from the project directory they are working on.
|
|
57
|
+
If unclear, ask the user what project they are working on.
|
|
58
|
+
|
|
59
|
+
Multi-Project Routing
|
|
60
|
+
---------------------
|
|
61
|
+
All tools (except list_projects) support an optional `project` parameter.
|
|
62
|
+
Use this to target a specific project when multiple GDSFactory+ servers are running.
|
|
63
|
+
|
|
64
|
+
If not specified, the first available server from the registry is used as a fallback.
|
|
65
|
+
You are adviced against ever using this fallback.
|
|
66
|
+
|
|
67
|
+
Designing Custom cells / factories
|
|
68
|
+
---------------------------------
|
|
69
|
+
Users may request you to build and design custom cells. Always consult the `list_samples` tool to discover examples of how to design cells.
|
|
70
|
+
Always make sure to run DRC/LVS/Connectivity checks on custom cells you have built.
|
|
71
|
+
Before running a DRC consult with the user if they would like to run the DRC, as these can take a long time to complete.
|
|
72
|
+
When users ask for specific characteristics of a cell / factory, you are to run simulations and use python scripts to analyze and verify the results.
|
|
73
|
+
Always write your new designs in python code, even though an example might show yaml.
|
|
74
|
+
Make sure to save your designs in project_name/custom_cells/**.py, and notify the user that they are saved here.
|
|
75
|
+
|
|
76
|
+
Best Practices
|
|
77
|
+
--------------
|
|
78
|
+
1. Always call `list_projects` first to discover available servers
|
|
79
|
+
2. Use the `project` parameter when working with multiple projects
|
|
80
|
+
3. For verification tools (DRC, connectivity, LVS), provide full paths to GDS files
|
|
81
|
+
4. Simulation results may be large - the server optimizes responses for token efficiency
|
|
82
|
+
5. When building cells, use the `visualize` parameter to get PNG previews
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_server(api_url: str | None = None) -> Server:
|
|
87
|
+
"""Create an MCP server instance.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Configured MCP Server instance
|
|
94
|
+
"""
|
|
95
|
+
server = Server("gdsfactoryplus", instructions=_INSTRUCTIONS.strip())
|
|
96
|
+
|
|
97
|
+
client = FastAPIClient(api_url)
|
|
98
|
+
|
|
99
|
+
@server.list_tools()
|
|
100
|
+
async def list_tools() -> list[Tool]:
|
|
101
|
+
"""List all available MCP tools.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of tool definitions
|
|
105
|
+
"""
|
|
106
|
+
tools = get_all_tools()
|
|
107
|
+
logger.info("Listing %d tools", len(tools))
|
|
108
|
+
return tools
|
|
109
|
+
|
|
110
|
+
@server.list_resources()
|
|
111
|
+
async def list_resources() -> list[Resource]:
|
|
112
|
+
"""List all available MCP resources.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of resource definitions
|
|
116
|
+
"""
|
|
117
|
+
resources = get_all_resources()
|
|
118
|
+
logger.info("Listing %d resources", len(resources))
|
|
119
|
+
return resources
|
|
120
|
+
|
|
121
|
+
@server.read_resource()
|
|
122
|
+
async def read_resource(uri: str) -> str:
|
|
123
|
+
"""Read a specific resource by URI.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
uri: Resource URI
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Resource content as string
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If resource URI is not found
|
|
133
|
+
"""
|
|
134
|
+
logger.info("Resource requested: %s", uri)
|
|
135
|
+
|
|
136
|
+
content = get_resource_content(uri)
|
|
137
|
+
if content is None:
|
|
138
|
+
error_msg = f"Unknown resource URI: {uri}"
|
|
139
|
+
logger.error(error_msg)
|
|
140
|
+
raise ValueError(error_msg)
|
|
141
|
+
|
|
142
|
+
return content
|
|
143
|
+
|
|
144
|
+
@server.call_tool()
|
|
145
|
+
async def call_tool(
|
|
146
|
+
name: str, arguments: dict[str, Any]
|
|
147
|
+
) -> list[TextContent | ImageContent]:
|
|
148
|
+
"""Call an MCP tool.
|
|
149
|
+
|
|
150
|
+
Uses handler dispatch to route tool calls to the appropriate handler.
|
|
151
|
+
Each handler encapsulates request/response transformation and execution.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Tool name
|
|
155
|
+
arguments: Tool arguments
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of text and/or image content responses
|
|
159
|
+
"""
|
|
160
|
+
logger.info("Tool called: %s", name)
|
|
161
|
+
logger.debug("Arguments: %s", arguments)
|
|
162
|
+
|
|
163
|
+
handler = get_handler(name)
|
|
164
|
+
if handler is None:
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
error_msg = f"Unknown tool: {name}"
|
|
168
|
+
logger.error(error_msg)
|
|
169
|
+
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
|
|
170
|
+
|
|
171
|
+
return await handler.handle(arguments, client)
|
|
172
|
+
|
|
173
|
+
server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
|
|
174
|
+
|
|
175
|
+
return server
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def run_server(api_url: str | None = None) -> None:
|
|
179
|
+
"""Run the MCP server with STDIO transport.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
183
|
+
"""
|
|
184
|
+
log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
|
|
185
|
+
logging.basicConfig(
|
|
186
|
+
level=log_level,
|
|
187
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
logger.info("Starting GDSFactory+ MCP server")
|
|
191
|
+
base_url_info = MCPConfig.get_api_url(api_url) or "registry-based routing"
|
|
192
|
+
logger.info("Base URL: %s", base_url_info)
|
|
193
|
+
logger.info("Timeout: %ds", MCPConfig.get_timeout())
|
|
194
|
+
|
|
195
|
+
server = create_server(api_url)
|
|
196
|
+
client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
await client.start()
|
|
200
|
+
|
|
201
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
202
|
+
logger.info("MCP server ready (STDIO transport)")
|
|
203
|
+
await server.run(
|
|
204
|
+
read_stream,
|
|
205
|
+
write_stream,
|
|
206
|
+
server.create_initialization_options(),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
except KeyboardInterrupt:
|
|
210
|
+
logger.info("Shutting down MCP server (keyboard interrupt)")
|
|
211
|
+
except Exception:
|
|
212
|
+
logger.exception("MCP server error")
|
|
213
|
+
raise
|
|
214
|
+
finally:
|
|
215
|
+
await client.close()
|
|
216
|
+
logger.info("MCP server stopped")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main(api_url: str | None = None) -> None:
|
|
220
|
+
"""Main entry point for MCP server.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
asyncio.run(run_server(api_url))
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
pass
|
|
229
|
+
except Exception:
|
|
230
|
+
logger.exception("Fatal error")
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""MCP tool definitions and handlers for GDSFactory+.
|
|
2
|
+
|
|
3
|
+
This package contains all tool handlers, each co-locating:
|
|
4
|
+
- MCP Tool definition (name, description, input schema)
|
|
5
|
+
- Request transformation (MCP args -> HTTP params/body)
|
|
6
|
+
- Response transformation (HTTP response -> MCP format)
|
|
7
|
+
- Execution logic (handle method)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .base import (
|
|
15
|
+
PROJECT_PARAM_SCHEMA,
|
|
16
|
+
EndpointMapping,
|
|
17
|
+
ToolHandler,
|
|
18
|
+
add_project_param,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Import all handlers
|
|
22
|
+
from .bbox import GenerateBboxHandler
|
|
23
|
+
from .build import BuildCellsHandler
|
|
24
|
+
from .cells import GetCellInfoHandler, ListCellsHandler
|
|
25
|
+
from .connectivity import CheckConnectivityHandler
|
|
26
|
+
from .drc import CheckDrcHandler
|
|
27
|
+
from .freeze import FreezeCellHandler
|
|
28
|
+
from .lvs import CheckLvsHandler
|
|
29
|
+
from .pdk import GetPdkInfoHandler
|
|
30
|
+
from .port import GetPortCenterHandler
|
|
31
|
+
from .project import GetProjectInfoHandler, ListProjectsHandler
|
|
32
|
+
from .samples import GetSampleFileHandler, ListSamplesHandler
|
|
33
|
+
from .simulation import SimulateComponentHandler
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from mcp.types import Tool
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Base classes
|
|
40
|
+
"EndpointMapping",
|
|
41
|
+
"ToolHandler",
|
|
42
|
+
# Registry and utilities
|
|
43
|
+
"TOOL_HANDLERS",
|
|
44
|
+
"get_all_tools",
|
|
45
|
+
"get_tool_by_name",
|
|
46
|
+
"get_handler",
|
|
47
|
+
"PROJECT_PARAM_SCHEMA",
|
|
48
|
+
"add_project_param",
|
|
49
|
+
# Handler classes
|
|
50
|
+
"ListProjectsHandler",
|
|
51
|
+
"GetProjectInfoHandler",
|
|
52
|
+
"BuildCellsHandler",
|
|
53
|
+
"ListCellsHandler",
|
|
54
|
+
"GetCellInfoHandler",
|
|
55
|
+
"CheckDrcHandler",
|
|
56
|
+
"CheckConnectivityHandler",
|
|
57
|
+
"CheckLvsHandler",
|
|
58
|
+
"SimulateComponentHandler",
|
|
59
|
+
"GetPortCenterHandler",
|
|
60
|
+
"GenerateBboxHandler",
|
|
61
|
+
"FreezeCellHandler",
|
|
62
|
+
"GetPdkInfoHandler",
|
|
63
|
+
"ListSamplesHandler",
|
|
64
|
+
"GetSampleFileHandler",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _create_handler_registry() -> dict[str, ToolHandler]:
|
|
69
|
+
"""Create the tool handlers registry.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dict mapping tool names to handler instances
|
|
73
|
+
"""
|
|
74
|
+
handlers = [
|
|
75
|
+
# Project discovery tools
|
|
76
|
+
ListProjectsHandler(),
|
|
77
|
+
GetProjectInfoHandler(),
|
|
78
|
+
# Core building tools
|
|
79
|
+
BuildCellsHandler(),
|
|
80
|
+
ListCellsHandler(),
|
|
81
|
+
GetCellInfoHandler(),
|
|
82
|
+
# Verification tools
|
|
83
|
+
CheckDrcHandler(),
|
|
84
|
+
CheckConnectivityHandler(),
|
|
85
|
+
CheckLvsHandler(),
|
|
86
|
+
# Advanced tools
|
|
87
|
+
SimulateComponentHandler(),
|
|
88
|
+
GetPortCenterHandler(),
|
|
89
|
+
GenerateBboxHandler(),
|
|
90
|
+
FreezeCellHandler(),
|
|
91
|
+
GetPdkInfoHandler(),
|
|
92
|
+
# Sample project tools
|
|
93
|
+
ListSamplesHandler(),
|
|
94
|
+
GetSampleFileHandler(),
|
|
95
|
+
]
|
|
96
|
+
return {handler.name: handler for handler in handlers}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Tool handlers registry - each handler is keyed by its tool name for O(1) lookup
|
|
100
|
+
TOOL_HANDLERS: dict[str, ToolHandler] = _create_handler_registry()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_all_tools() -> list[Tool]:
|
|
104
|
+
"""Get all available MCP tools.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of Tool definitions from all registered handlers
|
|
108
|
+
"""
|
|
109
|
+
return [handler.definition for handler in TOOL_HANDLERS.values()]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_tool_by_name(name: str) -> Tool | None:
|
|
113
|
+
"""Get a tool definition by name.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Tool name to look up
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tool definition if found, None otherwise
|
|
120
|
+
"""
|
|
121
|
+
handler = TOOL_HANDLERS.get(name)
|
|
122
|
+
return handler.definition if handler else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_handler(name: str) -> ToolHandler | None:
|
|
126
|
+
"""Get a tool handler by name.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Tool name to look up
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
ToolHandler instance if found, None otherwise
|
|
133
|
+
"""
|
|
134
|
+
return TOOL_HANDLERS.get(name)
|