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.
gfp_mcp/tools/base.py ADDED
@@ -0,0 +1,235 @@
1
+ """Base classes for tool handlers.
2
+
3
+ This module provides the foundational classes for the MCP tool handler
4
+ architecture, including the abstract ToolHandler base class and
5
+ EndpointMapping for HTTP routing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from mcp.types import ImageContent, TextContent, Tool
17
+
18
+ if TYPE_CHECKING:
19
+ from ..client import FastAPIClient
20
+
21
+ __all__ = [
22
+ "EndpointMapping",
23
+ "ToolHandler",
24
+ "PROJECT_PARAM_SCHEMA",
25
+ "add_project_param",
26
+ ]
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Common schema components for project routing
31
+ PROJECT_PARAM_SCHEMA = {
32
+ "project": {
33
+ "type": "string",
34
+ "description": (
35
+ "Optional project name or path to route this request to a specific server. "
36
+ "If not provided, uses the first available server from the registry or "
37
+ "GFP_API_URL if set. Use list_projects to see available projects."
38
+ ),
39
+ },
40
+ }
41
+
42
+
43
+ def add_project_param(schema: dict) -> dict:
44
+ """Add optional project parameter to a tool schema.
45
+
46
+ Args:
47
+ schema: Tool input schema dict
48
+
49
+ Returns:
50
+ Schema with project parameter added to properties
51
+ """
52
+ if "properties" not in schema:
53
+ schema["properties"] = {}
54
+ schema["properties"].update(PROJECT_PARAM_SCHEMA)
55
+ return schema
56
+
57
+
58
+ @dataclass
59
+ class EndpointMapping:
60
+ """Configuration for mapping an MCP tool to a FastAPI endpoint.
61
+
62
+ This class defines the HTTP method and path for a tool's backend endpoint.
63
+ Unlike the previous implementation in mappings.py, request and response
64
+ transformers are now methods on the ToolHandler class.
65
+
66
+ Attributes:
67
+ method: HTTP method (GET, POST, etc.)
68
+ path: API endpoint path (e.g., "/api/build-cells")
69
+ """
70
+
71
+ method: str
72
+ path: str
73
+
74
+
75
+ class ToolHandler(ABC):
76
+ """Abstract base class for MCP tool handlers.
77
+
78
+ Each tool handler encapsulates:
79
+ - The MCP Tool definition (name, description, input schema)
80
+ - The HTTP endpoint mapping (method, path)
81
+ - Request transformation (MCP args -> HTTP params/body)
82
+ - Response transformation (HTTP response -> MCP format)
83
+ - Execution logic (handle method)
84
+
85
+ Subclasses must implement the `name` and `definition` properties.
86
+ Other methods have sensible defaults but can be overridden.
87
+ """
88
+
89
+ @property
90
+ @abstractmethod
91
+ def name(self) -> str:
92
+ """Tool name identifier.
93
+
94
+ This must match the name in the Tool definition and is used
95
+ for routing in the server's call_tool handler.
96
+ """
97
+ ...
98
+
99
+ @property
100
+ @abstractmethod
101
+ def definition(self) -> Tool:
102
+ """MCP Tool definition.
103
+
104
+ Returns the complete Tool object with name, description,
105
+ and input schema for the MCP protocol.
106
+ """
107
+ ...
108
+
109
+ @property
110
+ def mapping(self) -> EndpointMapping | None:
111
+ """Endpoint mapping for HTTP-backed tools.
112
+
113
+ Returns None for registry-only tools (like list_projects)
114
+ that don't make HTTP requests to the backend.
115
+ """
116
+ return None
117
+
118
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
119
+ """Transform MCP tool arguments to HTTP request parameters.
120
+
121
+ Override this method to customize how tool arguments are
122
+ converted to HTTP request format.
123
+
124
+ Args:
125
+ args: MCP tool arguments from the client
126
+
127
+ Returns:
128
+ Dict that may contain:
129
+ - "params": Query parameters for GET requests
130
+ - "json_data": JSON body for POST requests
131
+ - "data": Form data for POST requests
132
+ - "path": Dynamic path override (e.g., "/api/cell/{name}")
133
+ - "fallback_path": Alternative endpoint for 404 fallback
134
+ - "fallback_json_data": Alternative body for fallback request
135
+ """
136
+ return {}
137
+
138
+ def transform_response(self, response: Any) -> Any:
139
+ """Transform HTTP response to MCP-friendly format.
140
+
141
+ Override this method to customize how backend responses are
142
+ formatted for the AI assistant.
143
+
144
+ Args:
145
+ response: Raw response from the FastAPI backend
146
+
147
+ Returns:
148
+ Transformed response (typically a dict or the original response)
149
+ """
150
+ return response
151
+
152
+ async def handle(
153
+ self,
154
+ arguments: dict[str, Any],
155
+ client: FastAPIClient,
156
+ ) -> list[TextContent | ImageContent]:
157
+ """Execute the tool and return results.
158
+
159
+ This is the main entry point called by the server's call_tool handler.
160
+ The default implementation uses the endpoint mapping and transformers.
161
+ Override for custom execution logic (e.g., registry-only tools).
162
+
163
+ Args:
164
+ arguments: MCP tool arguments from the client
165
+ client: FastAPI client for making HTTP requests
166
+
167
+ Returns:
168
+ List of TextContent and/or ImageContent responses
169
+ """
170
+ import httpx
171
+
172
+ mapping = self.mapping
173
+ if mapping is None:
174
+ error_msg = f"Tool {self.name} has no endpoint mapping"
175
+ logger.error(error_msg)
176
+ return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
177
+
178
+ try:
179
+ project = arguments.get("project")
180
+ transformed = self.transform_request(arguments)
181
+
182
+ method = mapping.method
183
+ path = transformed.get("path", mapping.path)
184
+ params = transformed.get("params")
185
+ json_data = transformed.get("json_data")
186
+ data = transformed.get("data")
187
+ fallback_path = transformed.get("fallback_path")
188
+ fallback_json_data = transformed.get("fallback_json_data")
189
+
190
+ try:
191
+ response = await client.request(
192
+ method=method,
193
+ path=path,
194
+ params=params,
195
+ json_data=json_data,
196
+ data=data,
197
+ project=project,
198
+ )
199
+ except httpx.HTTPStatusError as e:
200
+ if e.response.status_code == 404 and fallback_path:
201
+ logger.info(
202
+ "Endpoint %s returned 404, trying fallback: %s",
203
+ path,
204
+ fallback_path,
205
+ )
206
+ response = await client.request(
207
+ method=method,
208
+ path=fallback_path,
209
+ params=params,
210
+ json_data=fallback_json_data,
211
+ data=data,
212
+ project=project,
213
+ )
214
+ else:
215
+ raise
216
+
217
+ result = self.transform_response(response)
218
+ logger.debug("Tool %s result: %s", self.name, result)
219
+
220
+ return [
221
+ TextContent(
222
+ type="text",
223
+ text=json.dumps(result, indent=2),
224
+ )
225
+ ]
226
+
227
+ except Exception as e:
228
+ error_msg = f"Tool execution failed: {e!s}"
229
+ logger.exception(error_msg)
230
+ return [
231
+ TextContent(
232
+ type="text",
233
+ text=json.dumps({"error": error_msg}),
234
+ )
235
+ ]
gfp_mcp/tools/bbox.py ADDED
@@ -0,0 +1,115 @@
1
+ """Bounding box generation tool handler.
2
+
3
+ This module provides the handler for generating bounding box
4
+ GDS files from input GDS files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["GenerateBboxHandler"]
16
+
17
+
18
+ class GenerateBboxHandler(ToolHandler):
19
+ """Handler for generating bounding box GDS files.
20
+
21
+ Creates a simplified version of the layout with only a bounding box
22
+ on specified layers.
23
+ """
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "generate_bbox"
28
+
29
+ @property
30
+ def definition(self) -> Tool:
31
+ return Tool(
32
+ name="generate_bbox",
33
+ description=(
34
+ "Generate a bounding box GDS file from an input GDS. This creates "
35
+ "a simplified version of the layout with only a bounding box on "
36
+ "specified layers. Useful for creating abstract views, floorplanning, "
37
+ "or hierarchical design. Can optionally preserve specific layers and "
38
+ "ports."
39
+ ),
40
+ inputSchema=add_project_param(
41
+ {
42
+ "type": "object",
43
+ "properties": {
44
+ "path": {
45
+ "type": "string",
46
+ "description": (
47
+ "Path to the input GDS file. Can be absolute or relative "
48
+ "to the project directory."
49
+ ),
50
+ },
51
+ "outpath": {
52
+ "type": "string",
53
+ "description": (
54
+ "Output path for the bounding box GDS. If not specified, "
55
+ "uses the input filename with '-bbox' suffix."
56
+ ),
57
+ "default": "",
58
+ },
59
+ "layers_to_keep": {
60
+ "type": "array",
61
+ "items": {"type": "string"},
62
+ "description": (
63
+ "List of layer names to preserve in the output. "
64
+ "Other layers will be replaced by the bounding box."
65
+ ),
66
+ "default": [],
67
+ },
68
+ "bbox_layer": {
69
+ "type": "array",
70
+ "items": {"type": "integer"},
71
+ "description": (
72
+ "Layer (as [layer, datatype]) to use for the bounding box. "
73
+ "Default is [99, 0]."
74
+ ),
75
+ "default": [99, 0],
76
+ },
77
+ "ignore_ports": {
78
+ "type": "boolean",
79
+ "description": (
80
+ "If true, do not include ports in the output. "
81
+ "Default is false."
82
+ ),
83
+ "default": False,
84
+ },
85
+ },
86
+ "required": ["path"],
87
+ }
88
+ ),
89
+ )
90
+
91
+ @property
92
+ def mapping(self) -> EndpointMapping:
93
+ return EndpointMapping(method="POST", path="/api/bbox")
94
+
95
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
96
+ """Transform generate_bbox MCP args to FastAPI params.
97
+
98
+ Args:
99
+ args: MCP tool arguments
100
+
101
+ Returns:
102
+ Dict with 'json_data' key for request body
103
+ """
104
+ json_data: dict[str, Any] = {"path": args["path"]}
105
+
106
+ if "outpath" in args and args["outpath"]:
107
+ json_data["outpath"] = args["outpath"]
108
+ if "layers_to_keep" in args and args["layers_to_keep"]:
109
+ json_data["layers_to_keep"] = args["layers_to_keep"]
110
+ if "bbox_layer" in args and args["bbox_layer"]:
111
+ json_data["bbox_layer"] = args["bbox_layer"]
112
+ if "ignore_ports" in args:
113
+ json_data["ignore_ports"] = args["ignore_ports"]
114
+
115
+ return {"json_data": json_data}
gfp_mcp/tools/build.py ADDED
@@ -0,0 +1,159 @@
1
+ """Build cells tool handler.
2
+
3
+ This module provides the handler for building GDS cells, including
4
+ optional PNG image rendering for visualization.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from mcp.types import ImageContent, TextContent, Tool
14
+
15
+ from ..render import HAS_KLAYOUT, render_built_cells
16
+ from .base import EndpointMapping, ToolHandler, add_project_param
17
+
18
+ if TYPE_CHECKING:
19
+ from ..client import FastAPIClient
20
+
21
+ __all__ = ["BuildCellsHandler"]
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class BuildCellsHandler(ToolHandler):
27
+ """Handler for building GDS cells.
28
+
29
+ This handler builds one or more GDS cells and optionally renders
30
+ specified cells to PNG images for visualization.
31
+ """
32
+
33
+ @property
34
+ def name(self) -> str:
35
+ return "build_cells"
36
+
37
+ @property
38
+ def definition(self) -> Tool:
39
+ return Tool(
40
+ name="build_cells",
41
+ description=(
42
+ "Build one or more GDS cells by name. This creates the physical layout "
43
+ "files (.gds) for photonic components. Pass a list of cell names to build. "
44
+ "For a single cell, pass a list with one element. All cells are built "
45
+ "in the background and saved to the project build directory. "
46
+ "Use the optional 'visualize' parameter to specify which cells should be "
47
+ "rendered to PNG images and returned in the response."
48
+ ),
49
+ inputSchema=add_project_param(
50
+ {
51
+ "type": "object",
52
+ "properties": {
53
+ "names": {
54
+ "type": "array",
55
+ "items": {"type": "string"},
56
+ "description": "List of cell/component names to build (can be a single-item list)",
57
+ },
58
+ "visualize": {
59
+ "type": "array",
60
+ "items": {"type": "string"},
61
+ "description": (
62
+ "Optional list of cell names to render as PNG images. "
63
+ "Only cells in this list will be visualized. If not provided, "
64
+ "no images are returned. Use this to selectively view only "
65
+ "the cells you need to inspect."
66
+ ),
67
+ },
68
+ "with_metadata": {
69
+ "type": "boolean",
70
+ "description": (
71
+ "Include metadata in the GDS files (default: true)"
72
+ ),
73
+ "default": True,
74
+ },
75
+ "register": {
76
+ "type": "boolean",
77
+ "description": (
78
+ "Re-register the cells in the KLayout cache (default: true)"
79
+ ),
80
+ "default": True,
81
+ },
82
+ },
83
+ "required": ["names"],
84
+ }
85
+ ),
86
+ )
87
+
88
+ @property
89
+ def mapping(self) -> EndpointMapping:
90
+ return EndpointMapping(method="POST", path="/api/build-cells")
91
+
92
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
93
+ """Transform build_cells MCP args to FastAPI params.
94
+
95
+ Args:
96
+ args: MCP tool arguments
97
+
98
+ Returns:
99
+ Dict with 'params' key for query parameters and 'json_data' for body
100
+ """
101
+ return {
102
+ "params": {
103
+ "with_metadata": args.get("with_metadata", True),
104
+ "register": args.get("register", True),
105
+ },
106
+ "json_data": args["names"],
107
+ }
108
+
109
+ async def handle(
110
+ self,
111
+ arguments: dict[str, Any],
112
+ client: FastAPIClient,
113
+ ) -> list[TextContent | ImageContent]:
114
+ """Build cells and optionally render images.
115
+
116
+ Args:
117
+ arguments: MCP tool arguments
118
+ client: FastAPI client for making requests
119
+
120
+ Returns:
121
+ List of TextContent with build results and optional ImageContent
122
+ """
123
+ try:
124
+ project = arguments.get("project")
125
+ transformed = self.transform_request(arguments)
126
+
127
+ response = await client.request(
128
+ method=self.mapping.method,
129
+ path=self.mapping.path,
130
+ params=transformed.get("params"),
131
+ json_data=transformed.get("json_data"),
132
+ project=project,
133
+ )
134
+
135
+ logger.debug("Build cells result: %s", response)
136
+
137
+ results: list[TextContent | ImageContent] = [
138
+ TextContent(
139
+ type="text",
140
+ text=json.dumps(response, indent=2),
141
+ )
142
+ ]
143
+
144
+ # Render images if klayout is available and visualize is specified
145
+ visualize = arguments.get("visualize", [])
146
+ if visualize and HAS_KLAYOUT:
147
+ results.extend(await render_built_cells(visualize, project))
148
+
149
+ return results
150
+
151
+ except Exception as e:
152
+ error_msg = f"Tool execution failed: {e!s}"
153
+ logger.exception(error_msg)
154
+ return [
155
+ TextContent(
156
+ type="text",
157
+ text=json.dumps({"error": error_msg}),
158
+ )
159
+ ]
gfp_mcp/tools/cells.py ADDED
@@ -0,0 +1,103 @@
1
+ """Cell listing and info tool handlers.
2
+
3
+ These tools provide information about available cells/components
4
+ in the current PDK.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["ListCellsHandler", "GetCellInfoHandler"]
16
+
17
+
18
+ class ListCellsHandler(ToolHandler):
19
+ """Handler for listing all available cells/components."""
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ return "list_cells"
24
+
25
+ @property
26
+ def definition(self) -> Tool:
27
+ return Tool(
28
+ name="list_cells",
29
+ description=(
30
+ "List all available cells/components that can be built. Returns "
31
+ "the names of all registered component factories in the current PDK. "
32
+ "Use this to discover what components are available before building."
33
+ ),
34
+ inputSchema=add_project_param(
35
+ {
36
+ "type": "object",
37
+ "properties": {},
38
+ }
39
+ ),
40
+ )
41
+
42
+ @property
43
+ def mapping(self) -> EndpointMapping:
44
+ return EndpointMapping(method="GET", path="/api/cells")
45
+
46
+ def transform_response(self, response: Any) -> dict[str, Any]:
47
+ """Transform list_cells response to MCP format.
48
+
49
+ Args:
50
+ response: FastAPI response (list of cell names)
51
+
52
+ Returns:
53
+ Formatted response with cell names
54
+ """
55
+ if isinstance(response, list):
56
+ return {"cells": response, "count": len(response)}
57
+ return response
58
+
59
+
60
+ class GetCellInfoHandler(ToolHandler):
61
+ """Handler for getting detailed info about a specific cell."""
62
+
63
+ @property
64
+ def name(self) -> str:
65
+ return "get_cell_info"
66
+
67
+ @property
68
+ def definition(self) -> Tool:
69
+ return Tool(
70
+ name="get_cell_info",
71
+ description=(
72
+ "Get detailed information about a specific cell/component. Returns "
73
+ "metadata including the source file, parameters, and other details "
74
+ "about the component factory."
75
+ ),
76
+ inputSchema=add_project_param(
77
+ {
78
+ "type": "object",
79
+ "properties": {
80
+ "name": {
81
+ "type": "string",
82
+ "description": "Name of the cell/component to get info about",
83
+ },
84
+ },
85
+ "required": ["name"],
86
+ }
87
+ ),
88
+ )
89
+
90
+ @property
91
+ def mapping(self) -> EndpointMapping:
92
+ return EndpointMapping(method="GET", path="/api/cell-info")
93
+
94
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
95
+ """Transform get_cell_info MCP args to FastAPI params.
96
+
97
+ Args:
98
+ args: MCP tool arguments
99
+
100
+ Returns:
101
+ Dict with 'params' key for query parameters
102
+ """
103
+ return {"params": {"name": args["name"]}}
@@ -0,0 +1,70 @@
1
+ """Connectivity check tool handler.
2
+
3
+ This module provides the handler for running local connectivity
4
+ checks on GDS files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from mcp.types import Tool
12
+
13
+ from .base import EndpointMapping, ToolHandler, add_project_param
14
+
15
+ __all__ = ["CheckConnectivityHandler"]
16
+
17
+
18
+ class CheckConnectivityHandler(ToolHandler):
19
+ """Handler for running connectivity checks on GDS files.
20
+
21
+ This is a fast, local check that verifies all layers are properly
22
+ connected and identifies any connectivity violations.
23
+ """
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "check_connectivity"
28
+
29
+ @property
30
+ def definition(self) -> Tool:
31
+ return Tool(
32
+ name="check_connectivity",
33
+ description=(
34
+ "Run a local connectivity check on a GDS file. This verifies that "
35
+ "all layers are properly connected and identifies any connectivity "
36
+ "violations. This is a fast, local check (does not require uploading "
37
+ "to a remote server). Use this to quickly check for disconnected "
38
+ "components. Returns XML results showing connectivity issues."
39
+ ),
40
+ inputSchema=add_project_param(
41
+ {
42
+ "type": "object",
43
+ "properties": {
44
+ "path": {
45
+ "type": "string",
46
+ "description": (
47
+ "Path to the GDS file to check. Can be absolute or "
48
+ "relative to the project directory."
49
+ ),
50
+ },
51
+ },
52
+ "required": ["path"],
53
+ }
54
+ ),
55
+ )
56
+
57
+ @property
58
+ def mapping(self) -> EndpointMapping:
59
+ return EndpointMapping(method="POST", path="/api/check-connectivity")
60
+
61
+ def transform_request(self, args: dict[str, Any]) -> dict[str, Any]:
62
+ """Transform check_connectivity MCP args to FastAPI params.
63
+
64
+ Args:
65
+ args: MCP tool arguments
66
+
67
+ Returns:
68
+ Dict with 'json_data' key for request body
69
+ """
70
+ return {"json_data": {"path": args["path"]}}