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
gfp_mcp/server.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
The server uses a handler-based architecture where each tool has its own
|
|
7
|
+
handler class that encapsulates:
|
|
8
|
+
- Tool definition (name, description, input schema)
|
|
9
|
+
- Request transformation (MCP args -> HTTP params/body)
|
|
10
|
+
- Response transformation (HTTP response -> MCP format)
|
|
11
|
+
- Execution logic (handle method)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mcp.server import Server
|
|
21
|
+
from mcp.server.stdio import stdio_server
|
|
22
|
+
from mcp.types import (
|
|
23
|
+
ImageContent,
|
|
24
|
+
Resource,
|
|
25
|
+
TextContent,
|
|
26
|
+
Tool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .client import FastAPIClient
|
|
30
|
+
from .config import MCPConfig
|
|
31
|
+
from .resources import get_all_resources, get_resource_content
|
|
32
|
+
from .tools import get_all_tools, get_handler
|
|
33
|
+
|
|
34
|
+
__all__ = ["create_server", "run_server", "main"]
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_INSTRUCTIONS = """
|
|
40
|
+
GDSFactory+ MCP Server Instructions
|
|
41
|
+
===================================
|
|
42
|
+
|
|
43
|
+
This MCP server connects AI assistants to GDSFactory+ for photonic IC design.
|
|
44
|
+
Use this context to understand how to interact with the server effectively.
|
|
45
|
+
|
|
46
|
+
Server Discovery
|
|
47
|
+
----------------
|
|
48
|
+
Use the `list_projects` tool to discover running GDSFactory+ servers / projects.
|
|
49
|
+
Each project has its own server with a unique port.
|
|
50
|
+
|
|
51
|
+
If no servers are found, instruct the user to:
|
|
52
|
+
1. Open VSCode with the GDSFactory+ extension installed
|
|
53
|
+
2. Open a GDSFactory+ project folder
|
|
54
|
+
3. The extension will automatically start and register the server
|
|
55
|
+
|
|
56
|
+
Typically, users will invoke you from the project directory they are working on.
|
|
57
|
+
If unclear, ask the user what project they are working on.
|
|
58
|
+
|
|
59
|
+
Multi-Project Routing
|
|
60
|
+
---------------------
|
|
61
|
+
All tools (except list_projects) support an optional `project` parameter.
|
|
62
|
+
Use this to target a specific project when multiple GDSFactory+ servers are running.
|
|
63
|
+
|
|
64
|
+
If not specified, the first available server from the registry is used as a fallback.
|
|
65
|
+
You are adviced against ever using this fallback.
|
|
66
|
+
|
|
67
|
+
Designing Custom cells / factories
|
|
68
|
+
---------------------------------
|
|
69
|
+
Users may request you to build and design custom cells. Always consult the `list_samples` tool to discover examples of how to design cells.
|
|
70
|
+
Always make sure to run DRC/LVS/Connectivity checks on custom cells you have built.
|
|
71
|
+
Before running a DRC consult with the user if they would like to run the DRC, as these can take a long time to complete.
|
|
72
|
+
When users ask for specific characteristics of a cell / factory, you are to run simulations and use python scripts to analyze and verify the results.
|
|
73
|
+
Always write your new designs in python code, even though an example might show yaml.
|
|
74
|
+
Make sure to save your designs in project_name/custom_cells/**.py, and notify the user that they are saved here.
|
|
75
|
+
|
|
76
|
+
Best Practices
|
|
77
|
+
--------------
|
|
78
|
+
1. Always call `list_projects` first to discover available servers
|
|
79
|
+
2. Use the `project` parameter when working with multiple projects
|
|
80
|
+
3. For verification tools (DRC, connectivity, LVS), provide full paths to GDS files
|
|
81
|
+
4. Simulation results may be large - the server optimizes responses for token efficiency
|
|
82
|
+
5. When building cells, use the `visualize` parameter to get PNG previews
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_server(api_url: str | None = None) -> Server:
|
|
87
|
+
"""Create an MCP server instance.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Configured MCP Server instance
|
|
94
|
+
"""
|
|
95
|
+
server = Server("gdsfactoryplus", instructions=_INSTRUCTIONS.strip())
|
|
96
|
+
|
|
97
|
+
client = FastAPIClient(api_url)
|
|
98
|
+
|
|
99
|
+
@server.list_tools()
|
|
100
|
+
async def list_tools() -> list[Tool]:
|
|
101
|
+
"""List all available MCP tools.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of tool definitions
|
|
105
|
+
"""
|
|
106
|
+
tools = get_all_tools()
|
|
107
|
+
logger.info("Listing %d tools", len(tools))
|
|
108
|
+
return tools
|
|
109
|
+
|
|
110
|
+
@server.list_resources()
|
|
111
|
+
async def list_resources() -> list[Resource]:
|
|
112
|
+
"""List all available MCP resources.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of resource definitions
|
|
116
|
+
"""
|
|
117
|
+
resources = get_all_resources()
|
|
118
|
+
logger.info("Listing %d resources", len(resources))
|
|
119
|
+
return resources
|
|
120
|
+
|
|
121
|
+
@server.read_resource()
|
|
122
|
+
async def read_resource(uri: str) -> str:
|
|
123
|
+
"""Read a specific resource by URI.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
uri: Resource URI
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Resource content as string
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If resource URI is not found
|
|
133
|
+
"""
|
|
134
|
+
logger.info("Resource requested: %s", uri)
|
|
135
|
+
|
|
136
|
+
content = get_resource_content(uri)
|
|
137
|
+
if content is None:
|
|
138
|
+
error_msg = f"Unknown resource URI: {uri}"
|
|
139
|
+
logger.error(error_msg)
|
|
140
|
+
raise ValueError(error_msg)
|
|
141
|
+
|
|
142
|
+
return content
|
|
143
|
+
|
|
144
|
+
@server.call_tool()
|
|
145
|
+
async def call_tool(
|
|
146
|
+
name: str, arguments: dict[str, Any]
|
|
147
|
+
) -> list[TextContent | ImageContent]:
|
|
148
|
+
"""Call an MCP tool.
|
|
149
|
+
|
|
150
|
+
Uses handler dispatch to route tool calls to the appropriate handler.
|
|
151
|
+
Each handler encapsulates request/response transformation and execution.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Tool name
|
|
155
|
+
arguments: Tool arguments
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of text and/or image content responses
|
|
159
|
+
"""
|
|
160
|
+
logger.info("Tool called: %s", name)
|
|
161
|
+
logger.debug("Arguments: %s", arguments)
|
|
162
|
+
|
|
163
|
+
handler = get_handler(name)
|
|
164
|
+
if handler is None:
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
error_msg = f"Unknown tool: {name}"
|
|
168
|
+
logger.error(error_msg)
|
|
169
|
+
return [TextContent(type="text", text=json.dumps({"error": error_msg}))]
|
|
170
|
+
|
|
171
|
+
return await handler.handle(arguments, client)
|
|
172
|
+
|
|
173
|
+
server._http_client = client # type: ignore[attr-defined] # noqa: SLF001
|
|
174
|
+
|
|
175
|
+
return server
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def run_server(api_url: str | None = None) -> None:
|
|
179
|
+
"""Run the MCP server with STDIO transport.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
183
|
+
"""
|
|
184
|
+
log_level = logging.DEBUG if MCPConfig.DEBUG else logging.INFO
|
|
185
|
+
logging.basicConfig(
|
|
186
|
+
level=log_level,
|
|
187
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
logger.info("Starting GDSFactory+ MCP server")
|
|
191
|
+
base_url_info = MCPConfig.get_api_url(api_url) or "registry-based routing"
|
|
192
|
+
logger.info("Base URL: %s", base_url_info)
|
|
193
|
+
logger.info("Timeout: %ds", MCPConfig.get_timeout())
|
|
194
|
+
|
|
195
|
+
server = create_server(api_url)
|
|
196
|
+
client: FastAPIClient = server._http_client # type: ignore[attr-defined] # noqa: SLF001
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
await client.start()
|
|
200
|
+
|
|
201
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
202
|
+
logger.info("MCP server ready (STDIO transport)")
|
|
203
|
+
await server.run(
|
|
204
|
+
read_stream,
|
|
205
|
+
write_stream,
|
|
206
|
+
server.create_initialization_options(),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
except KeyboardInterrupt:
|
|
210
|
+
logger.info("Shutting down MCP server (keyboard interrupt)")
|
|
211
|
+
except Exception:
|
|
212
|
+
logger.exception("MCP server error")
|
|
213
|
+
raise
|
|
214
|
+
finally:
|
|
215
|
+
await client.close()
|
|
216
|
+
logger.info("MCP server stopped")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main(api_url: str | None = None) -> None:
|
|
220
|
+
"""Main entry point for MCP server.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
api_url: Optional FastAPI base URL (default from config)
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
asyncio.run(run_server(api_url))
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
pass
|
|
229
|
+
except Exception:
|
|
230
|
+
logger.exception("Fatal error")
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""MCP tool definitions and handlers for GDSFactory+.
|
|
2
|
+
|
|
3
|
+
This package contains all tool handlers, each co-locating:
|
|
4
|
+
- MCP Tool definition (name, description, input schema)
|
|
5
|
+
- Request transformation (MCP args -> HTTP params/body)
|
|
6
|
+
- Response transformation (HTTP response -> MCP format)
|
|
7
|
+
- Execution logic (handle method)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .base import (
|
|
15
|
+
PROJECT_PARAM_SCHEMA,
|
|
16
|
+
EndpointMapping,
|
|
17
|
+
ToolHandler,
|
|
18
|
+
add_project_param,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Import all handlers
|
|
22
|
+
from .bbox import GenerateBboxHandler
|
|
23
|
+
from .build import BuildCellsHandler
|
|
24
|
+
from .cells import GetCellInfoHandler, ListCellsHandler
|
|
25
|
+
from .connectivity import CheckConnectivityHandler
|
|
26
|
+
from .drc import CheckDrcHandler
|
|
27
|
+
from .freeze import FreezeCellHandler
|
|
28
|
+
from .lvs import CheckLvsHandler
|
|
29
|
+
from .pdk import GetPdkInfoHandler
|
|
30
|
+
from .port import GetPortCenterHandler
|
|
31
|
+
from .project import GetProjectInfoHandler, ListProjectsHandler
|
|
32
|
+
from .samples import GetSampleFileHandler, ListSamplesHandler
|
|
33
|
+
from .simulation import SimulateComponentHandler
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from mcp.types import Tool
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Base classes
|
|
40
|
+
"EndpointMapping",
|
|
41
|
+
"ToolHandler",
|
|
42
|
+
# Registry and utilities
|
|
43
|
+
"TOOL_HANDLERS",
|
|
44
|
+
"get_all_tools",
|
|
45
|
+
"get_tool_by_name",
|
|
46
|
+
"get_handler",
|
|
47
|
+
"PROJECT_PARAM_SCHEMA",
|
|
48
|
+
"add_project_param",
|
|
49
|
+
# Handler classes
|
|
50
|
+
"ListProjectsHandler",
|
|
51
|
+
"GetProjectInfoHandler",
|
|
52
|
+
"BuildCellsHandler",
|
|
53
|
+
"ListCellsHandler",
|
|
54
|
+
"GetCellInfoHandler",
|
|
55
|
+
"CheckDrcHandler",
|
|
56
|
+
"CheckConnectivityHandler",
|
|
57
|
+
"CheckLvsHandler",
|
|
58
|
+
"SimulateComponentHandler",
|
|
59
|
+
"GetPortCenterHandler",
|
|
60
|
+
"GenerateBboxHandler",
|
|
61
|
+
"FreezeCellHandler",
|
|
62
|
+
"GetPdkInfoHandler",
|
|
63
|
+
"ListSamplesHandler",
|
|
64
|
+
"GetSampleFileHandler",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _create_handler_registry() -> dict[str, ToolHandler]:
|
|
69
|
+
"""Create the tool handlers registry.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dict mapping tool names to handler instances
|
|
73
|
+
"""
|
|
74
|
+
handlers = [
|
|
75
|
+
# Project discovery tools
|
|
76
|
+
ListProjectsHandler(),
|
|
77
|
+
GetProjectInfoHandler(),
|
|
78
|
+
# Core building tools
|
|
79
|
+
BuildCellsHandler(),
|
|
80
|
+
ListCellsHandler(),
|
|
81
|
+
GetCellInfoHandler(),
|
|
82
|
+
# Verification tools
|
|
83
|
+
CheckDrcHandler(),
|
|
84
|
+
CheckConnectivityHandler(),
|
|
85
|
+
CheckLvsHandler(),
|
|
86
|
+
# Advanced tools
|
|
87
|
+
SimulateComponentHandler(),
|
|
88
|
+
GetPortCenterHandler(),
|
|
89
|
+
GenerateBboxHandler(),
|
|
90
|
+
FreezeCellHandler(),
|
|
91
|
+
GetPdkInfoHandler(),
|
|
92
|
+
# Sample project tools
|
|
93
|
+
ListSamplesHandler(),
|
|
94
|
+
GetSampleFileHandler(),
|
|
95
|
+
]
|
|
96
|
+
return {handler.name: handler for handler in handlers}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Tool handlers registry - each handler is keyed by its tool name for O(1) lookup
|
|
100
|
+
TOOL_HANDLERS: dict[str, ToolHandler] = _create_handler_registry()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_all_tools() -> list[Tool]:
|
|
104
|
+
"""Get all available MCP tools.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of Tool definitions from all registered handlers
|
|
108
|
+
"""
|
|
109
|
+
return [handler.definition for handler in TOOL_HANDLERS.values()]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_tool_by_name(name: str) -> Tool | None:
|
|
113
|
+
"""Get a tool definition by name.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Tool name to look up
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tool definition if found, None otherwise
|
|
120
|
+
"""
|
|
121
|
+
handler = TOOL_HANDLERS.get(name)
|
|
122
|
+
return handler.definition if handler else None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_handler(name: str) -> ToolHandler | None:
|
|
126
|
+
"""Get a tool handler by name.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Tool name to look up
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
ToolHandler instance if found, None otherwise
|
|
133
|
+
"""
|
|
134
|
+
return TOOL_HANDLERS.get(name)
|
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
|
+
]
|