gfp-mcp 0.1.0__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.
@@ -0,0 +1,228 @@
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
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ from typing import Any
13
+
14
+ from mcp.server import Server
15
+ from mcp.server.stdio import stdio_server
16
+ from mcp.types import TextContent, Tool
17
+
18
+ from .client import FastAPIClient
19
+ from .config import MCPConfig
20
+ from .mappings import get_mapping, transform_request, transform_response
21
+ from .tools import get_all_tools
22
+
23
+ __all__ = ["create_server", "run_server", "main"]
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def create_server(api_url: str | None = None) -> Server:
29
+ """Create an MCP server instance.
30
+
31
+ Args:
32
+ api_url: Optional FastAPI base URL (default from config)
33
+
34
+ Returns:
35
+ Configured MCP Server instance
36
+ """
37
+ # Create server instance
38
+ server = Server("gdsfactoryplus")
39
+
40
+ # Create HTTP client for FastAPI backend
41
+ client = FastAPIClient(api_url)
42
+
43
+ @server.list_tools()
44
+ async def list_tools() -> list[Tool]:
45
+ """List all available MCP tools.
46
+
47
+ Returns:
48
+ List of tool definitions
49
+ """
50
+ tools = get_all_tools()
51
+ logger.info("Listing %d tools", len(tools))
52
+ return tools
53
+
54
+ @server.call_tool()
55
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: # noqa: PLR0911
56
+ """Call an MCP tool.
57
+
58
+ Args:
59
+ name: Tool name
60
+ arguments: Tool arguments
61
+
62
+ Returns:
63
+ List of text content responses
64
+ """
65
+ logger.info("Tool called: %s", name)
66
+ logger.debug("Arguments: %s", arguments)
67
+
68
+ # Handle special tools that don't require HTTP requests
69
+ if name == "list_projects":
70
+ try:
71
+ projects = client.list_projects()
72
+ return [
73
+ TextContent(
74
+ type="text",
75
+ text=json.dumps({"projects": projects}, indent=2),
76
+ )
77
+ ]
78
+ except Exception as e:
79
+ error_msg = f"Failed to list projects: {e!s}"
80
+ logger.exception(error_msg)
81
+ return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
82
+
83
+ if name == "get_project_info":
84
+ try:
85
+ project = arguments.get("project")
86
+ if not project:
87
+ return [
88
+ TextContent(
89
+ type="text",
90
+ text=json.dumps({"error": "project parameter is required"}),
91
+ )
92
+ ]
93
+ info = await client.get_project_info(project)
94
+ return [TextContent(type="text", text=json.dumps(info, indent=2))]
95
+ except Exception as e:
96
+ error_msg = f"Failed to get project info: {e!s}"
97
+ logger.exception(error_msg)
98
+ return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
99
+
100
+ # Get the endpoint mapping
101
+ mapping = get_mapping(name)
102
+ if mapping is None:
103
+ error_msg = f"Unknown tool: {name}"
104
+ logger.error(error_msg)
105
+ return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
106
+
107
+ try:
108
+ # Extract project parameter (optional)
109
+ project = arguments.get("project")
110
+
111
+ # Transform MCP arguments to HTTP request parameters
112
+ transformed = transform_request(name, arguments)
113
+ logger.debug("Transformed request: %s", transformed)
114
+
115
+ # Extract request parameters
116
+ method = mapping.method
117
+ path = transformed.get("path", mapping.path)
118
+ params = transformed.get("params")
119
+ json_data = transformed.get("json_data")
120
+ data = transformed.get("data")
121
+
122
+ # Make HTTP request to FastAPI backend (with optional project routing)
123
+ response = await client.request(
124
+ method=method,
125
+ path=path,
126
+ params=params,
127
+ json_data=json_data,
128
+ data=data,
129
+ project=project,
130
+ )
131
+
132
+ # Transform response to MCP format
133
+ result = transform_response(name, response)
134
+ logger.debug("Tool result: %s", result)
135
+
136
+ # Return as text content
137
+ return [
138
+ TextContent(
139
+ type="text",
140
+ text=json.dumps(result, indent=2),
141
+ )
142
+ ]
143
+
144
+ except Exception as e:
145
+ error_msg = f"Tool execution failed: {e!s}"
146
+ logger.exception(error_msg)
147
+ return [
148
+ TextContent(
149
+ type="text",
150
+ text=json.dumps({"error": error_msg}),
151
+ )
152
+ ]
153
+
154
+ # Store client reference for cleanup
155
+ server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
156
+
157
+ return server
158
+
159
+
160
+ async def run_server(api_url: str | None = None) -> None:
161
+ """Run the MCP server with STDIO transport.
162
+
163
+ Args:
164
+ api_url: Optional FastAPI base URL (default from config)
165
+ """
166
+ # Configure logging
167
+ log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
168
+ logging.basicConfig(
169
+ level=log_level,
170
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
171
+ )
172
+
173
+ logger.info("Starting GDSFactory+ MCP server")
174
+ logger.info("FastAPI base URL: %s", MCPConfig.get_api_url(api_url))
175
+ logger.info("Timeout: %ds", MCPConfig.get_timeout())
176
+
177
+ # Create server
178
+ server = create_server(api_url)
179
+ client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
180
+
181
+ try:
182
+ # Start HTTP client
183
+ await client.start()
184
+
185
+ # Health check
186
+ healthy = await client.health_check()
187
+ if not healthy:
188
+ logger.warning(
189
+ "FastAPI server health check failed. Server may not be running."
190
+ )
191
+
192
+ # Run server with STDIO transport
193
+ async with stdio_server() as (read_stream, write_stream):
194
+ logger.info("MCP server ready (STDIO transport)")
195
+ await server.run(
196
+ read_stream,
197
+ write_stream,
198
+ server.create_initialization_options(),
199
+ )
200
+
201
+ except KeyboardInterrupt:
202
+ logger.info("Shutting down MCP server (keyboard interrupt)")
203
+ except Exception:
204
+ logger.exception("MCP server error")
205
+ raise
206
+ finally:
207
+ # Cleanup
208
+ await client.close()
209
+ logger.info("MCP server stopped")
210
+
211
+
212
+ def main(api_url: str | None = None) -> None:
213
+ """Main entry point for MCP server.
214
+
215
+ Args:
216
+ api_url: Optional FastAPI base URL (default from config)
217
+ """
218
+ try:
219
+ asyncio.run(run_server(api_url))
220
+ except KeyboardInterrupt:
221
+ pass
222
+ except Exception:
223
+ logger.exception("Fatal error")
224
+ raise
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()
@@ -0,0 +1,368 @@
1
+ """MCP tool definitions for GDSFactory+.
2
+
3
+ This module defines the MCP tools that are exposed to AI assistants.
4
+ Tools are organized by functionality and phase of implementation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mcp.types import Tool
10
+
11
+ __all__ = [
12
+ "TOOLS",
13
+ "get_all_tools",
14
+ "get_tool_by_name",
15
+ ]
16
+
17
+
18
+ # Project discovery tools (for multi-project support)
19
+ PROJECT_TOOLS: list[Tool] = [
20
+ Tool(
21
+ name="list_projects",
22
+ description=(
23
+ "List all active GDSFactory+ projects. Returns information about "
24
+ "all running servers including project name, path, port, PID, and PDK. "
25
+ "Use this to discover which projects are available for interaction."
26
+ ),
27
+ inputSchema={
28
+ "type": "object",
29
+ "properties": {},
30
+ },
31
+ ),
32
+ Tool(
33
+ name="get_project_info",
34
+ description=(
35
+ "Get detailed information about a specific project. Returns metadata "
36
+ "including project name, path, port, PID, PDK, and version. "
37
+ "Use this to get information about a running project."
38
+ ),
39
+ inputSchema={
40
+ "type": "object",
41
+ "properties": {
42
+ "project": {
43
+ "type": "string",
44
+ "description": (
45
+ "Project name or path. Can be the project directory name "
46
+ "or full path."
47
+ ),
48
+ },
49
+ },
50
+ "required": ["project"],
51
+ },
52
+ ),
53
+ ]
54
+
55
+ # Standard optional project parameter for all tools
56
+ PROJECT_PARAM_SCHEMA = {
57
+ "project": {
58
+ "type": "string",
59
+ "description": (
60
+ "Optional project name or path to route this request to a specific "
61
+ "server. If not provided, uses the default server (port 8787). "
62
+ "Use list_projects to see available projects."
63
+ ),
64
+ },
65
+ }
66
+
67
+
68
+ def _add_project_param(schema: dict) -> dict:
69
+ """Add optional project parameter to a tool schema.
70
+
71
+ Args:
72
+ schema: Original input schema
73
+
74
+ Returns:
75
+ Schema with project parameter added
76
+ """
77
+ if "properties" not in schema:
78
+ schema["properties"] = {}
79
+ schema["properties"].update(PROJECT_PARAM_SCHEMA)
80
+ return schema
81
+
82
+
83
+ # Phase 1: Core Building Tools (5 tools)
84
+ 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
+ Tool(
121
+ name="build_cells",
122
+ description=(
123
+ "Build multiple GDS cells by name in one operation. This is more "
124
+ "efficient than building cells one at a time. All cells are built "
125
+ "in the background and saved to the project build directory."
126
+ ),
127
+ inputSchema=_add_project_param(
128
+ {
129
+ "type": "object",
130
+ "properties": {
131
+ "names": {
132
+ "type": "array",
133
+ "items": {"type": "string"},
134
+ "description": "List of cell/component names to build",
135
+ },
136
+ "with_metadata": {
137
+ "type": "boolean",
138
+ "description": (
139
+ "Include metadata in the GDS files (default: true)"
140
+ ),
141
+ "default": True,
142
+ },
143
+ "register": {
144
+ "type": "boolean",
145
+ "description": (
146
+ "Re-register the cells in the KLayout cache (default: true)"
147
+ ),
148
+ "default": True,
149
+ },
150
+ },
151
+ "required": ["names"],
152
+ }
153
+ ),
154
+ ),
155
+ Tool(
156
+ name="list_cells",
157
+ description=(
158
+ "List all available cells/components that can be built. Returns "
159
+ "the names of all registered component factories in the current PDK. "
160
+ "Use this to discover what components are available before building."
161
+ ),
162
+ inputSchema=_add_project_param(
163
+ {
164
+ "type": "object",
165
+ "properties": {},
166
+ }
167
+ ),
168
+ ),
169
+ Tool(
170
+ name="get_cell_info",
171
+ description=(
172
+ "Get detailed information about a specific cell/component. Returns "
173
+ "metadata including the source file, parameters, and other details "
174
+ "about the component factory."
175
+ ),
176
+ inputSchema=_add_project_param(
177
+ {
178
+ "type": "object",
179
+ "properties": {
180
+ "name": {
181
+ "type": "string",
182
+ "description": "Name of the cell/component to get info about",
183
+ },
184
+ },
185
+ "required": ["name"],
186
+ }
187
+ ),
188
+ ),
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
+ ]
213
+
214
+ # Phase 2: Verification Tools
215
+ VERIFICATION_TOOLS: list[Tool] = [
216
+ Tool(
217
+ name="check_drc",
218
+ description=(
219
+ "Run a full DRC (Design Rule Check) on a GDS file. This uploads "
220
+ "the file to a remote DRC server and runs comprehensive design rule "
221
+ "verification for the specified PDK and process. Use this for "
222
+ "complete design rule validation. Returns XML results showing all "
223
+ "DRC violations."
224
+ ),
225
+ inputSchema=_add_project_param(
226
+ {
227
+ "type": "object",
228
+ "properties": {
229
+ "path": {
230
+ "type": "string",
231
+ "description": (
232
+ "Path to the GDS file to check. Can be absolute or "
233
+ "relative to the project directory."
234
+ ),
235
+ },
236
+ "pdk": {
237
+ "type": "string",
238
+ "description": (
239
+ "PDK to use for the check. If not specified, uses the "
240
+ "default PDK from settings."
241
+ ),
242
+ },
243
+ "process": {
244
+ "type": "string",
245
+ "description": (
246
+ "Process variant for DRC rules. If not specified, uses "
247
+ "the default process from settings."
248
+ ),
249
+ },
250
+ "timeout": {
251
+ "type": "integer",
252
+ "description": (
253
+ "Timeout in seconds for the DRC check. If not specified, "
254
+ "uses the default timeout from settings."
255
+ ),
256
+ },
257
+ "host": {
258
+ "type": "string",
259
+ "description": (
260
+ "API host for the DRC server. If not specified, uses "
261
+ "the default host from settings."
262
+ ),
263
+ },
264
+ },
265
+ "required": ["path"],
266
+ }
267
+ ),
268
+ ),
269
+ Tool(
270
+ name="check_connectivity",
271
+ description=(
272
+ "Run a local connectivity check on a GDS file. This verifies that "
273
+ "all layers are properly connected and identifies any connectivity "
274
+ "violations. This is a fast, local check (does not require uploading "
275
+ "to a remote server). Use this to quickly check for disconnected "
276
+ "components. Returns XML results showing connectivity issues."
277
+ ),
278
+ inputSchema=_add_project_param(
279
+ {
280
+ "type": "object",
281
+ "properties": {
282
+ "path": {
283
+ "type": "string",
284
+ "description": (
285
+ "Path to the GDS file to check. Can be absolute or "
286
+ "relative to the project directory."
287
+ ),
288
+ },
289
+ },
290
+ "required": ["path"],
291
+ }
292
+ ),
293
+ ),
294
+ Tool(
295
+ name="check_lvs",
296
+ description=(
297
+ "Run LVS (Layout vs. Schematic) verification on a cell against a "
298
+ "reference netlist. This compares the physical layout to the "
299
+ "schematic representation to ensure they match. Returns XML results "
300
+ "showing any mismatches between layout and schematic."
301
+ ),
302
+ inputSchema=_add_project_param(
303
+ {
304
+ "type": "object",
305
+ "properties": {
306
+ "cell": {
307
+ "type": "string",
308
+ "description": "Name of the cell to verify",
309
+ },
310
+ "netpath": {
311
+ "type": "string",
312
+ "description": (
313
+ "Path to the reference netlist file to compare against"
314
+ ),
315
+ },
316
+ "cellargs": {
317
+ "type": "string",
318
+ "description": (
319
+ "Optional cell arguments as a JSON string. "
320
+ "Default is empty string."
321
+ ),
322
+ "default": "",
323
+ },
324
+ },
325
+ "required": ["cell", "netpath"],
326
+ }
327
+ ),
328
+ ),
329
+ ]
330
+
331
+ # Phase 3: SPICE Workflow Tools (to be implemented)
332
+ SPICE_TOOLS: list[Tool] = []
333
+
334
+ # Phase 4: Simulation & Advanced Tools (to be implemented)
335
+ ADVANCED_TOOLS: list[Tool] = []
336
+
337
+ # All tools (Phase 1 + Project tools)
338
+ TOOLS: list[Tool] = [
339
+ *PROJECT_TOOLS, # Project discovery tools (always available)
340
+ *CORE_TOOLS,
341
+ *VERIFICATION_TOOLS,
342
+ *SPICE_TOOLS,
343
+ *ADVANCED_TOOLS,
344
+ ]
345
+
346
+
347
+ def get_all_tools() -> list[Tool]:
348
+ """Get all available MCP tools.
349
+
350
+ Returns:
351
+ List of all tool definitions
352
+ """
353
+ return TOOLS
354
+
355
+
356
+ def get_tool_by_name(name: str) -> Tool | None:
357
+ """Get a tool by name.
358
+
359
+ Args:
360
+ name: Tool name
361
+
362
+ Returns:
363
+ Tool definition or None if not found
364
+ """
365
+ for tool in TOOLS:
366
+ if tool.name == name:
367
+ return tool
368
+ return None