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/server.py DELETED
@@ -1,294 +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
- # Create server instance
44
- server = Server("gdsfactoryplus")
45
-
46
- # Create HTTP client for FastAPI backend
47
- client = FastAPIClient(api_url)
48
-
49
- @server.list_tools()
50
- async def list_tools() -> list[Tool]:
51
- """List all available MCP tools.
52
-
53
- Returns:
54
- List of tool definitions
55
- """
56
- tools = get_all_tools()
57
- logger.info("Listing %d tools", len(tools))
58
- return tools
59
-
60
- @server.list_resources()
61
- async def list_resources() -> list[Resource]:
62
- """List all available MCP resources.
63
-
64
- Returns:
65
- List of resource definitions
66
- """
67
- resources = get_all_resources()
68
- logger.info("Listing %d resources", len(resources))
69
- return resources
70
-
71
- @server.read_resource()
72
- async def read_resource(uri: str) -> str:
73
- """Read a specific resource by URI.
74
-
75
- Args:
76
- uri: Resource URI
77
-
78
- Returns:
79
- Resource content as string
80
-
81
- Raises:
82
- ValueError: If resource URI is not found
83
- """
84
- logger.info("Resource requested: %s", uri)
85
-
86
- # Get resource content
87
- content = get_resource_content(uri)
88
- if content is None:
89
- error_msg = f"Unknown resource URI: {uri}"
90
- logger.error(error_msg)
91
- raise ValueError(error_msg)
92
-
93
- return content
94
-
95
- @server.call_tool()
96
- async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: # noqa: PLR0911
97
- """Call an MCP tool.
98
-
99
- Args:
100
- name: Tool name
101
- arguments: Tool arguments
102
-
103
- Returns:
104
- List of text content responses
105
- """
106
- logger.info("Tool called: %s", name)
107
- logger.debug("Arguments: %s", arguments)
108
-
109
- # Handle special tools that don't require HTTP requests
110
- if name == "list_projects":
111
- try:
112
- projects = client.list_projects()
113
- return [
114
- TextContent(
115
- type="text",
116
- text=json.dumps({"projects": projects}, indent=2),
117
- )
118
- ]
119
- except Exception as e:
120
- error_msg = f"Failed to list projects: {e!s}"
121
- logger.exception(error_msg)
122
- return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
123
-
124
- if name == "get_project_info":
125
- try:
126
- project = arguments.get("project")
127
- if not project:
128
- return [
129
- TextContent(
130
- type="text",
131
- text=json.dumps({"error": "project parameter is required"}),
132
- )
133
- ]
134
- info = await client.get_project_info(project)
135
- return [TextContent(type="text", text=json.dumps(info, indent=2))]
136
- except Exception as e:
137
- error_msg = f"Failed to get project info: {e!s}"
138
- logger.exception(error_msg)
139
- return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
140
-
141
- # Get the endpoint mapping
142
- mapping = get_mapping(name)
143
- if mapping is None:
144
- error_msg = f"Unknown tool: {name}"
145
- logger.error(error_msg)
146
- return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
147
-
148
- try:
149
- # Extract project parameter (optional)
150
- project = arguments.get("project")
151
-
152
- # Transform MCP arguments to HTTP request parameters
153
- transformed = transform_request(name, arguments)
154
- logger.debug("Transformed request: %s", transformed)
155
-
156
- # Extract request parameters
157
- method = mapping.method
158
- path = transformed.get("path", mapping.path)
159
- params = transformed.get("params")
160
- json_data = transformed.get("json_data")
161
- data = transformed.get("data")
162
-
163
- # Make HTTP request to FastAPI backend (with optional project routing)
164
- response = await client.request(
165
- method=method,
166
- path=path,
167
- params=params,
168
- json_data=json_data,
169
- data=data,
170
- project=project,
171
- )
172
-
173
- # Transform response to MCP format
174
- result = transform_response(name, response)
175
- logger.debug("Tool result: %s", result)
176
-
177
- # Return as text content
178
- return [
179
- TextContent(
180
- type="text",
181
- text=json.dumps(result, indent=2),
182
- )
183
- ]
184
-
185
- except Exception as e:
186
- error_msg = f"Tool execution failed: {e!s}"
187
- logger.exception(error_msg)
188
- return [
189
- TextContent(
190
- type="text",
191
- text=json.dumps({"error": error_msg}),
192
- )
193
- ]
194
-
195
- # Store client reference for cleanup
196
- server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
197
-
198
- return server
199
-
200
-
201
- def _log_server_discovery(client: FastAPIClient) -> None:
202
- """Log discovered GDSFactory+ servers at startup."""
203
- try:
204
- projects = client.list_projects()
205
- if projects:
206
- logger.info("Discovered %d GDSFactory+ server(s):", len(projects))
207
- for project in projects[:3]: # Show first 3
208
- logger.info(
209
- " - %s (port %d, PDK: %s)",
210
- project["project_name"],
211
- project["port"],
212
- project.get("pdk", "unknown"),
213
- )
214
- if len(projects) > 3:
215
- logger.info(" ... and %d more", len(projects) - 3)
216
- else:
217
- logger.warning(
218
- "No GDSFactory+ servers found. Start a server with: "
219
- "gfp serve --port 8787"
220
- )
221
- logger.info("Registry location: %s", get_registry_path())
222
- except Exception as e:
223
- logger.warning("Could not read server registry: %s", e)
224
- logger.info(
225
- "If GDSFactory+ is running, ensure registry is accessible at: %s",
226
- get_registry_path(),
227
- )
228
-
229
-
230
- async def run_server(api_url: str | None = None) -> None:
231
- """Run the MCP server with STDIO transport.
232
-
233
- Args:
234
- api_url: Optional FastAPI base URL (default from config)
235
- """
236
- # Configure logging
237
- log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
238
- logging.basicConfig(
239
- level=log_level,
240
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
241
- )
242
-
243
- logger.info("Starting GDSFactory+ MCP server")
244
- logger.info("FastAPI base URL: %s", MCPConfig.get_api_url(api_url))
245
- logger.info("Timeout: %ds", MCPConfig.get_timeout())
246
-
247
- # Create server
248
- server = create_server(api_url)
249
- client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
250
-
251
- try:
252
- # Start HTTP client
253
- await client.start()
254
-
255
- # Discover available servers
256
- _log_server_discovery(client)
257
-
258
- # Run server with STDIO transport
259
- async with stdio_server() as (read_stream, write_stream):
260
- logger.info("MCP server ready (STDIO transport)")
261
- await server.run(
262
- read_stream,
263
- write_stream,
264
- server.create_initialization_options(),
265
- )
266
-
267
- except KeyboardInterrupt:
268
- logger.info("Shutting down MCP server (keyboard interrupt)")
269
- except Exception:
270
- logger.exception("MCP server error")
271
- raise
272
- finally:
273
- # Cleanup
274
- await client.close()
275
- logger.info("MCP server stopped")
276
-
277
-
278
- def main(api_url: str | None = None) -> None:
279
- """Main entry point for MCP server.
280
-
281
- Args:
282
- api_url: Optional FastAPI base URL (default from config)
283
- """
284
- try:
285
- asyncio.run(run_server(api_url))
286
- except KeyboardInterrupt:
287
- pass
288
- except Exception:
289
- logger.exception("Fatal error")
290
- raise
291
-
292
-
293
- if __name__ == "__main__":
294
- main()