gfp-mcp 0.1.0__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.
mcp_standalone/server.py CHANGED
@@ -13,11 +13,17 @@ from typing import Any
13
13
 
14
14
  from mcp.server import Server
15
15
  from mcp.server.stdio import stdio_server
16
- from mcp.types import TextContent, Tool
16
+ from mcp.types import (
17
+ Resource,
18
+ TextContent,
19
+ Tool,
20
+ )
17
21
 
18
22
  from .client import FastAPIClient
19
23
  from .config import MCPConfig
20
24
  from .mappings import get_mapping, transform_request, transform_response
25
+ from .registry import get_registry_path
26
+ from .resources import get_all_resources, get_resource_content
21
27
  from .tools import get_all_tools
22
28
 
23
29
  __all__ = ["create_server", "run_server", "main"]
@@ -34,10 +40,8 @@ def create_server(api_url: str | None = None) -> Server:
34
40
  Returns:
35
41
  Configured MCP Server instance
36
42
  """
37
- # Create server instance
38
43
  server = Server("gdsfactoryplus")
39
44
 
40
- # Create HTTP client for FastAPI backend
41
45
  client = FastAPIClient(api_url)
42
46
 
43
47
  @server.list_tools()
@@ -51,6 +55,40 @@ def create_server(api_url: str | None = None) -> Server:
51
55
  logger.info("Listing %d tools", len(tools))
52
56
  return tools
53
57
 
58
+ @server.list_resources()
59
+ async def list_resources() -> list[Resource]:
60
+ """List all available MCP resources.
61
+
62
+ Returns:
63
+ List of resource definitions
64
+ """
65
+ resources = get_all_resources()
66
+ logger.info("Listing %d resources", len(resources))
67
+ return resources
68
+
69
+ @server.read_resource()
70
+ async def read_resource(uri: str) -> str:
71
+ """Read a specific resource by URI.
72
+
73
+ Args:
74
+ uri: Resource URI
75
+
76
+ Returns:
77
+ Resource content as string
78
+
79
+ Raises:
80
+ ValueError: If resource URI is not found
81
+ """
82
+ logger.info("Resource requested: %s", uri)
83
+
84
+ content = get_resource_content(uri)
85
+ if content is None:
86
+ error_msg = f"Unknown resource URI: {uri}"
87
+ logger.error(error_msg)
88
+ raise ValueError(error_msg)
89
+
90
+ return content
91
+
54
92
  @server.call_tool()
55
93
  async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: # noqa: PLR0911
56
94
  """Call an MCP tool.
@@ -65,7 +103,6 @@ def create_server(api_url: str | None = None) -> Server:
65
103
  logger.info("Tool called: %s", name)
66
104
  logger.debug("Arguments: %s", arguments)
67
105
 
68
- # Handle special tools that don't require HTTP requests
69
106
  if name == "list_projects":
70
107
  try:
71
108
  projects = client.list_projects()
@@ -97,7 +134,6 @@ def create_server(api_url: str | None = None) -> Server:
97
134
  logger.exception(error_msg)
98
135
  return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
99
136
 
100
- # Get the endpoint mapping
101
137
  mapping = get_mapping(name)
102
138
  if mapping is None:
103
139
  error_msg = f"Unknown tool: {name}"
@@ -105,21 +141,17 @@ def create_server(api_url: str | None = None) -> Server:
105
141
  return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
106
142
 
107
143
  try:
108
- # Extract project parameter (optional)
109
144
  project = arguments.get("project")
110
145
 
111
- # Transform MCP arguments to HTTP request parameters
112
146
  transformed = transform_request(name, arguments)
113
147
  logger.debug("Transformed request: %s", transformed)
114
148
 
115
- # Extract request parameters
116
149
  method = mapping.method
117
150
  path = transformed.get("path", mapping.path)
118
151
  params = transformed.get("params")
119
152
  json_data = transformed.get("json_data")
120
153
  data = transformed.get("data")
121
154
 
122
- # Make HTTP request to FastAPI backend (with optional project routing)
123
155
  response = await client.request(
124
156
  method=method,
125
157
  path=path,
@@ -129,11 +161,9 @@ def create_server(api_url: str | None = None) -> Server:
129
161
  project=project,
130
162
  )
131
163
 
132
- # Transform response to MCP format
133
164
  result = transform_response(name, response)
134
165
  logger.debug("Tool result: %s", result)
135
166
 
136
- # Return as text content
137
167
  return [
138
168
  TextContent(
139
169
  type="text",
@@ -151,19 +181,51 @@ def create_server(api_url: str | None = None) -> Server:
151
181
  )
152
182
  ]
153
183
 
154
- # Store client reference for cleanup
155
184
  server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
156
185
 
157
186
  return server
158
187
 
159
188
 
189
+ def _log_server_discovery(client: FastAPIClient) -> None:
190
+ """Log discovered GDSFactory+ servers at startup."""
191
+ try:
192
+ projects = client.list_projects()
193
+ if projects:
194
+ logger.info("Discovered %d GDSFactory+ server(s):", len(projects))
195
+ for project in projects[:3]: # Show first 3
196
+ logger.info(
197
+ " - %s (port %d, PDK: %s)",
198
+ project["project_name"],
199
+ project["port"],
200
+ project.get("pdk", "unknown"),
201
+ )
202
+ if len(projects) > 3:
203
+ logger.info(" ... and %d more", len(projects) - 3)
204
+ else:
205
+ logger.warning(
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"
210
+ )
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
+ )
215
+ except Exception as e:
216
+ logger.warning("Could not read server registry: %s", e)
217
+ logger.info(
218
+ "If GDSFactory+ is running, ensure registry is accessible at: %s",
219
+ get_registry_path(),
220
+ )
221
+
222
+
160
223
  async def run_server(api_url: str | None = None) -> None:
161
224
  """Run the MCP server with STDIO transport.
162
225
 
163
226
  Args:
164
227
  api_url: Optional FastAPI base URL (default from config)
165
228
  """
166
- # Configure logging
167
229
  log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
168
230
  logging.basicConfig(
169
231
  level=log_level,
@@ -171,25 +233,18 @@ async def run_server(api_url: str | None = None) -> None:
171
233
  )
172
234
 
173
235
  logger.info("Starting GDSFactory+ MCP server")
174
- logger.info("FastAPI base URL: %s", MCPConfig.get_api_url(api_url))
236
+ base_url_info = MCPConfig.get_api_url(api_url) or "registry-based routing"
237
+ logger.info("Base URL: %s", base_url_info)
175
238
  logger.info("Timeout: %ds", MCPConfig.get_timeout())
176
239
 
177
- # Create server
178
240
  server = create_server(api_url)
179
241
  client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
180
242
 
181
243
  try:
182
- # Start HTTP client
183
244
  await client.start()
184
245
 
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
- )
246
+ _log_server_discovery(client)
191
247
 
192
- # Run server with STDIO transport
193
248
  async with stdio_server() as (read_stream, write_stream):
194
249
  logger.info("MCP server ready (STDIO transport)")
195
250
  await server.run(
@@ -204,7 +259,6 @@ async def run_server(api_url: str | None = None) -> None:
204
259
  logger.exception("MCP server error")
205
260
  raise
206
261
  finally:
207
- # Cleanup
208
262
  await client.close()
209
263
  logger.info("MCP server stopped")
210
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, uses the default server (port 8787). "
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 multiple GDS cells by name in one operation. This is more "
124
- "efficient than building cells one at a time. All cells are built "
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,13 +267,172 @@ 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 (to be implemented)
335
- ADVANCED_TOOLS: list[Tool] = []
272
+ ADVANCED_TOOLS: list[Tool] = [
273
+ Tool(
274
+ name="simulate_component",
275
+ description=(
276
+ "Run a SAX circuit simulation on a photonic component by name. "
277
+ "This simulates the optical behavior of the component using its "
278
+ "SAX model. Returns S-parameters showing how light propagates "
279
+ "through the component's ports. Use this to analyze component "
280
+ "performance before fabrication."
281
+ ),
282
+ inputSchema=_add_project_param(
283
+ {
284
+ "type": "object",
285
+ "properties": {
286
+ "name": {
287
+ "type": "string",
288
+ "description": (
289
+ "Name of the component/cell to simulate. The component "
290
+ "must have a SAX model defined."
291
+ ),
292
+ },
293
+ },
294
+ "required": ["name"],
295
+ }
296
+ ),
297
+ ),
298
+ Tool(
299
+ name="get_port_center",
300
+ description=(
301
+ "Get the center coordinates (x, y) of a specific port in a component "
302
+ "instance. This is useful for positioning components, routing waveguides, "
303
+ "or analyzing layout geometry. Returns the physical coordinates in "
304
+ "microns."
305
+ ),
306
+ inputSchema=_add_project_param(
307
+ {
308
+ "type": "object",
309
+ "properties": {
310
+ "netlist": {
311
+ "type": "string",
312
+ "description": (
313
+ "Name of the component/netlist containing the instance"
314
+ ),
315
+ },
316
+ "instance": {
317
+ "type": "string",
318
+ "description": "Name of the instance within the netlist",
319
+ },
320
+ "port": {
321
+ "type": "string",
322
+ "description": "Name of the port to get coordinates for",
323
+ },
324
+ },
325
+ "required": ["netlist", "instance", "port"],
326
+ }
327
+ ),
328
+ ),
329
+ Tool(
330
+ name="generate_bbox",
331
+ description=(
332
+ "Generate a bounding box GDS file from an input GDS. This creates "
333
+ "a simplified version of the layout with only a bounding box on "
334
+ "specified layers. Useful for creating abstract views, floorplanning, "
335
+ "or hierarchical design. Can optionally preserve specific layers and "
336
+ "ports."
337
+ ),
338
+ inputSchema=_add_project_param(
339
+ {
340
+ "type": "object",
341
+ "properties": {
342
+ "path": {
343
+ "type": "string",
344
+ "description": (
345
+ "Path to the input GDS file. Can be absolute or relative "
346
+ "to the project directory."
347
+ ),
348
+ },
349
+ "outpath": {
350
+ "type": "string",
351
+ "description": (
352
+ "Output path for the bounding box GDS. If not specified, "
353
+ "uses the input filename with '-bbox' suffix."
354
+ ),
355
+ "default": "",
356
+ },
357
+ "layers_to_keep": {
358
+ "type": "array",
359
+ "items": {"type": "string"},
360
+ "description": (
361
+ "List of layer names to preserve in the output. "
362
+ "Other layers will be replaced by the bounding box."
363
+ ),
364
+ "default": [],
365
+ },
366
+ "bbox_layer": {
367
+ "type": "array",
368
+ "items": {"type": "integer"},
369
+ "description": (
370
+ "Layer (as [layer, datatype]) to use for the bounding box. "
371
+ "Default is [99, 0]."
372
+ ),
373
+ "default": [99, 0],
374
+ },
375
+ "ignore_ports": {
376
+ "type": "boolean",
377
+ "description": (
378
+ "If true, do not include ports in the output. "
379
+ "Default is false."
380
+ ),
381
+ "default": False,
382
+ },
383
+ },
384
+ "required": ["path"],
385
+ }
386
+ ),
387
+ ),
388
+ Tool(
389
+ name="freeze_cell",
390
+ description=(
391
+ "Freeze a parametric Python cell as a static schematic netlist. "
392
+ "This converts a gdsfactory component with specific parameters into "
393
+ "a fixed netlist representation in YAML format. Useful for creating "
394
+ "versioned snapshots of parametric designs or preparing components "
395
+ "for simulation workflows."
396
+ ),
397
+ inputSchema=_add_project_param(
398
+ {
399
+ "type": "object",
400
+ "properties": {
401
+ "cell_name": {
402
+ "type": "string",
403
+ "description": "Name of the cell/component to freeze",
404
+ },
405
+ "kwargs": {
406
+ "type": "object",
407
+ "description": (
408
+ "Optional keyword arguments to pass to the component "
409
+ "factory. Use this to specify parameter values when "
410
+ "freezing the cell. Default is empty (use default params)."
411
+ ),
412
+ "default": {},
413
+ },
414
+ },
415
+ "required": ["cell_name"],
416
+ }
417
+ ),
418
+ ),
419
+ Tool(
420
+ name="get_pdk_info",
421
+ description=(
422
+ "Get information about the current PDK (Process Design Kit) in use. "
423
+ "Returns metadata including PDK name, project name, project path, "
424
+ "server port, and version. Use this to verify which PDK is active "
425
+ "and get project configuration details."
426
+ ),
427
+ inputSchema=_add_project_param(
428
+ {
429
+ "type": "object",
430
+ "properties": {},
431
+ }
432
+ ),
433
+ ),
434
+ ]
336
435
 
337
- # All tools (Phase 1 + Project tools)
338
436
  TOOLS: list[Tool] = [
339
437
  *PROJECT_TOOLS, # Project discovery tools (always available)
340
438
  *CORE_TOOLS,