gfp-mcp 0.2.4__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 +0 -14
- gfp_mcp/config.py +161 -0
- gfp_mcp/render.py +139 -0
- 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.4.dist-info → gfp_mcp-0.3.2.dist-info}/METADATA +13 -1
- 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.4.dist-info/RECORD +0 -14
- gfp_mcp-0.2.4.dist-info/entry_points.txt +0 -2
- gfp_mcp-0.2.4.dist-info/top_level.txt +0 -1
- mcp_standalone/config.py +0 -50
- mcp_standalone/mappings.py +0 -565
- mcp_standalone/server.py +0 -282
- mcp_standalone/tools.py +0 -466
- {mcp_standalone → gfp_mcp}/registry.py +0 -0
- {mcp_standalone → gfp_mcp}/resources.py +0 -0
- {gfp_mcp-0.2.4.dist-info → gfp_mcp-0.3.2.dist-info}/WHEEL +0 -0
- {gfp_mcp-0.2.4.dist-info → gfp_mcp-0.3.2.dist-info}/licenses/LICENSE +0 -0
mcp_standalone/server.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
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 (
|
|
17
|
-
Resource,
|
|
18
|
-
TextContent,
|
|
19
|
-
Tool,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
from .client import FastAPIClient
|
|
23
|
-
from .config import MCPConfig
|
|
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
|
|
27
|
-
from .tools import get_all_tools
|
|
28
|
-
|
|
29
|
-
__all__ = ["create_server", "run_server", "main"]
|
|
30
|
-
|
|
31
|
-
logger = logging.getLogger(__name__)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def create_server(api_url: str | None = None) -> Server:
|
|
35
|
-
"""Create an MCP server instance.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
api_url: Optional FastAPI base URL (default from config)
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
Configured MCP Server instance
|
|
42
|
-
"""
|
|
43
|
-
server = Server("gdsfactoryplus")
|
|
44
|
-
|
|
45
|
-
client = FastAPIClient(api_url)
|
|
46
|
-
|
|
47
|
-
@server.list_tools()
|
|
48
|
-
async def list_tools() -> list[Tool]:
|
|
49
|
-
"""List all available MCP tools.
|
|
50
|
-
|
|
51
|
-
Returns:
|
|
52
|
-
List of tool definitions
|
|
53
|
-
"""
|
|
54
|
-
tools = get_all_tools()
|
|
55
|
-
logger.info("Listing %d tools", len(tools))
|
|
56
|
-
return tools
|
|
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
|
-
|
|
92
|
-
@server.call_tool()
|
|
93
|
-
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: # noqa: PLR0911
|
|
94
|
-
"""Call an MCP tool.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
name: Tool name
|
|
98
|
-
arguments: Tool arguments
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
List of text content responses
|
|
102
|
-
"""
|
|
103
|
-
logger.info("Tool called: %s", name)
|
|
104
|
-
logger.debug("Arguments: %s", arguments)
|
|
105
|
-
|
|
106
|
-
if name == "list_projects":
|
|
107
|
-
try:
|
|
108
|
-
projects = client.list_projects()
|
|
109
|
-
return [
|
|
110
|
-
TextContent(
|
|
111
|
-
type="text",
|
|
112
|
-
text=json.dumps({"projects": projects}, indent=2),
|
|
113
|
-
)
|
|
114
|
-
]
|
|
115
|
-
except Exception as e:
|
|
116
|
-
error_msg = f"Failed to list projects: {e!s}"
|
|
117
|
-
logger.exception(error_msg)
|
|
118
|
-
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
|
|
119
|
-
|
|
120
|
-
if name == "get_project_info":
|
|
121
|
-
try:
|
|
122
|
-
project = arguments.get("project")
|
|
123
|
-
if not project:
|
|
124
|
-
return [
|
|
125
|
-
TextContent(
|
|
126
|
-
type="text",
|
|
127
|
-
text=json.dumps({"error": "project parameter is required"}),
|
|
128
|
-
)
|
|
129
|
-
]
|
|
130
|
-
info = await client.get_project_info(project)
|
|
131
|
-
return [TextContent(type="text", text=json.dumps(info, indent=2))]
|
|
132
|
-
except Exception as e:
|
|
133
|
-
error_msg = f"Failed to get project info: {e!s}"
|
|
134
|
-
logger.exception(error_msg)
|
|
135
|
-
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
|
|
136
|
-
|
|
137
|
-
mapping = get_mapping(name)
|
|
138
|
-
if mapping is None:
|
|
139
|
-
error_msg = f"Unknown tool: {name}"
|
|
140
|
-
logger.error(error_msg)
|
|
141
|
-
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
project = arguments.get("project")
|
|
145
|
-
|
|
146
|
-
transformed = transform_request(name, arguments)
|
|
147
|
-
logger.debug("Transformed request: %s", transformed)
|
|
148
|
-
|
|
149
|
-
method = mapping.method
|
|
150
|
-
path = transformed.get("path", mapping.path)
|
|
151
|
-
params = transformed.get("params")
|
|
152
|
-
json_data = transformed.get("json_data")
|
|
153
|
-
data = transformed.get("data")
|
|
154
|
-
|
|
155
|
-
response = await client.request(
|
|
156
|
-
method=method,
|
|
157
|
-
path=path,
|
|
158
|
-
params=params,
|
|
159
|
-
json_data=json_data,
|
|
160
|
-
data=data,
|
|
161
|
-
project=project,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
result = transform_response(name, response)
|
|
165
|
-
logger.debug("Tool result: %s", result)
|
|
166
|
-
|
|
167
|
-
return [
|
|
168
|
-
TextContent(
|
|
169
|
-
type="text",
|
|
170
|
-
text=json.dumps(result, indent=2),
|
|
171
|
-
)
|
|
172
|
-
]
|
|
173
|
-
|
|
174
|
-
except Exception as e:
|
|
175
|
-
error_msg = f"Tool execution failed: {e!s}"
|
|
176
|
-
logger.exception(error_msg)
|
|
177
|
-
return [
|
|
178
|
-
TextContent(
|
|
179
|
-
type="text",
|
|
180
|
-
text=json.dumps({"error": error_msg}),
|
|
181
|
-
)
|
|
182
|
-
]
|
|
183
|
-
|
|
184
|
-
server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
|
|
185
|
-
|
|
186
|
-
return server
|
|
187
|
-
|
|
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
|
-
|
|
223
|
-
async def run_server(api_url: str | None = None) -> None:
|
|
224
|
-
"""Run the MCP server with STDIO transport.
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
api_url: Optional FastAPI base URL (default from config)
|
|
228
|
-
"""
|
|
229
|
-
log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
|
|
230
|
-
logging.basicConfig(
|
|
231
|
-
level=log_level,
|
|
232
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
logger.info("Starting GDSFactory+ MCP server")
|
|
236
|
-
base_url_info = MCPConfig.get_api_url(api_url) or "registry-based routing"
|
|
237
|
-
logger.info("Base URL: %s", base_url_info)
|
|
238
|
-
logger.info("Timeout: %ds", MCPConfig.get_timeout())
|
|
239
|
-
|
|
240
|
-
server = create_server(api_url)
|
|
241
|
-
client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
|
|
242
|
-
|
|
243
|
-
try:
|
|
244
|
-
await client.start()
|
|
245
|
-
|
|
246
|
-
_log_server_discovery(client)
|
|
247
|
-
|
|
248
|
-
async with stdio_server() as (read_stream, write_stream):
|
|
249
|
-
logger.info("MCP server ready (STDIO transport)")
|
|
250
|
-
await server.run(
|
|
251
|
-
read_stream,
|
|
252
|
-
write_stream,
|
|
253
|
-
server.create_initialization_options(),
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
except KeyboardInterrupt:
|
|
257
|
-
logger.info("Shutting down MCP server (keyboard interrupt)")
|
|
258
|
-
except Exception:
|
|
259
|
-
logger.exception("MCP server error")
|
|
260
|
-
raise
|
|
261
|
-
finally:
|
|
262
|
-
await client.close()
|
|
263
|
-
logger.info("MCP server stopped")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def main(api_url: str | None = None) -> None:
|
|
267
|
-
"""Main entry point for MCP server.
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
api_url: Optional FastAPI base URL (default from config)
|
|
271
|
-
"""
|
|
272
|
-
try:
|
|
273
|
-
asyncio.run(run_server(api_url))
|
|
274
|
-
except KeyboardInterrupt:
|
|
275
|
-
pass
|
|
276
|
-
except Exception:
|
|
277
|
-
logger.exception("Fatal error")
|
|
278
|
-
raise
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if __name__ == "__main__":
|
|
282
|
-
main()
|