gfp-mcp 0.1.0__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-0.1.0.dist-info/METADATA +360 -0
- gfp_mcp-0.1.0.dist-info/RECORD +12 -0
- gfp_mcp-0.1.0.dist-info/WHEEL +5 -0
- gfp_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- gfp_mcp-0.1.0.dist-info/top_level.txt +1 -0
- mcp_standalone/__init__.py +39 -0
- mcp_standalone/client.py +268 -0
- mcp_standalone/config.py +54 -0
- mcp_standalone/mappings.py +286 -0
- mcp_standalone/registry.py +210 -0
- mcp_standalone/server.py +228 -0
- mcp_standalone/tools.py +368 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gfp-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model Context Protocol (MCP) server for GDSFactory+ photonic IC design
|
|
5
|
+
Author: GDSFactory+ Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/doplaydo/gfp-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/doplaydo/gfp-mcp
|
|
9
|
+
Project-URL: Documentation, https://github.com/doplaydo/gfp-mcp#readme
|
|
10
|
+
Project-URL: Changelog, https://github.com/doplaydo/gfp-mcp/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Issue Tracker, https://github.com/doplaydo/gfp-mcp/issues
|
|
12
|
+
Keywords: mcp,gdsfactory,photonics,ic-design,eda,model-context-protocol,photonic-ic,gds
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: mcp>=1.7.1
|
|
27
|
+
Requires-Dist: httpx>=0.25.0
|
|
28
|
+
Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
|
|
29
|
+
Requires-Dist: psutil>=5.9.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
|
+
|
|
36
|
+
# GDSFactory+ MCP Server
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/gfp-mcp/)
|
|
39
|
+
[](https://pypi.org/project/gfp-mcp/)
|
|
40
|
+
[](https://github.com/doplaydo/gfp-mcp/actions)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
Model Context Protocol (MCP) server for GDSFactory+ that exposes photonic IC design operations as tools for AI assistants like Claude Code and Claude Desktop.
|
|
44
|
+
|
|
45
|
+
## Overview
|
|
46
|
+
|
|
47
|
+
This project implements a standalone MCP server that bridges AI assistants with GDSFactory+ photonic IC design capabilities. The server acts as a lightweight proxy that exposes GDSFactory+ operations through standardized MCP tools while maintaining zero modifications to the existing FastAPI backend.
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
### Phase 1: Core Building Tools (Complete)
|
|
52
|
+
|
|
53
|
+
- **build_cell** - Build a single GDS cell by name
|
|
54
|
+
- **build_cells** - Build multiple GDS cells in batch
|
|
55
|
+
- **list_cells** - List all available photonic components
|
|
56
|
+
- **get_cell_info** - Get detailed component metadata
|
|
57
|
+
- **download_gds** - Download built GDS files
|
|
58
|
+
- **list_projects** - List all running GDSFactory+ server instances
|
|
59
|
+
- **get_project_info** - Get detailed information about a specific project
|
|
60
|
+
|
|
61
|
+
### Multi-Project Support
|
|
62
|
+
|
|
63
|
+
The MCP server integrates with the GDSFactory+ server registry to support working with multiple projects simultaneously. The registry is stored at `~/.gdsfactory/server-registry.json` and is automatically managed by GDSFactory+ servers.
|
|
64
|
+
|
|
65
|
+
#### How It Works
|
|
66
|
+
|
|
67
|
+
1. When you start a GDSFactory+ server with `gfp serve`, it registers itself in the shared registry
|
|
68
|
+
2. The MCP server reads from this registry to discover available projects
|
|
69
|
+
3. Tools accept an optional `project` parameter to route requests to specific servers
|
|
70
|
+
4. The MCP server automatically resolves project names to the correct port
|
|
71
|
+
|
|
72
|
+
#### Example Usage
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
User: "List all running GDSFactory+ projects"
|
|
76
|
+
Claude: [Uses list_projects tool to show all registered servers]
|
|
77
|
+
|
|
78
|
+
User: "Build the mzi component in the my_photonics_project"
|
|
79
|
+
Claude: [Uses build_cell tool with project="my_photonics_project"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Note**: The MCP has read-only access to the registry. Only GDSFactory+ servers can register/unregister themselves.
|
|
83
|
+
|
|
84
|
+
### Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
AI Assistant (Claude) <-> MCP Server (STDIO) <-> HTTP Client <-> FastAPI Server
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Key Benefits:**
|
|
91
|
+
- Universal MCP client compatibility via STDIO transport
|
|
92
|
+
- Zero modifications to existing FastAPI server
|
|
93
|
+
- Clean separation of concerns
|
|
94
|
+
- No database conflicts (only FastAPI touches SQLite)
|
|
95
|
+
- Scalable architecture ready for 20+ tools
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
### From PyPI (Recommended)
|
|
100
|
+
|
|
101
|
+
Install the package from PyPI:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install gfp-mcp
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or with uv:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv pip install gfp-mcp
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### From Source (Development)
|
|
114
|
+
|
|
115
|
+
For development or if you want the latest unreleased changes:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/doplaydo/gfp-mcp.git
|
|
119
|
+
cd gfp-mcp
|
|
120
|
+
pip install -e ".[dev]"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Prerequisites
|
|
124
|
+
|
|
125
|
+
- Python 3.10 or higher
|
|
126
|
+
- GDSFactory+ with FastAPI server running
|
|
127
|
+
|
|
128
|
+
### Verify Installation
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
gfp-mcp-serve --help
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Quick Start
|
|
135
|
+
|
|
136
|
+
### 1. Start the FastAPI Server
|
|
137
|
+
|
|
138
|
+
In one terminal:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
gfp serve --port 8787
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Configure Your AI Assistant
|
|
145
|
+
|
|
146
|
+
#### Claude Code
|
|
147
|
+
|
|
148
|
+
Add to `.claude/settings.json`:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"mcpServers": {
|
|
153
|
+
"gdsfactoryplus": {
|
|
154
|
+
"command": "gfp-mcp-serve",
|
|
155
|
+
"args": [],
|
|
156
|
+
"env": {
|
|
157
|
+
"GFP_API_URL": "http://localhost:8787"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or use the command line:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
claude mcp add gdsfactoryplus -- gfp-mcp-serve
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Claude Desktop
|
|
171
|
+
|
|
172
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"mcpServers": {
|
|
177
|
+
"gdsfactoryplus": {
|
|
178
|
+
"command": "gfp-mcp-serve",
|
|
179
|
+
"args": []
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
For Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
186
|
+
|
|
187
|
+
For Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
188
|
+
|
|
189
|
+
### 3. Use the Tools
|
|
190
|
+
|
|
191
|
+
Ask your AI assistant to:
|
|
192
|
+
|
|
193
|
+
- "List all available photonic components"
|
|
194
|
+
- "Build the mzi component"
|
|
195
|
+
- "Show me details about the coupler component"
|
|
196
|
+
- "Build multiple components: mzi, coupler, and bend_euler"
|
|
197
|
+
|
|
198
|
+
## Environment Variables
|
|
199
|
+
|
|
200
|
+
Configure the MCP server using environment variables:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# FastAPI server URL (default: http://localhost:8787)
|
|
204
|
+
export GFP_API_URL="http://localhost:8787"
|
|
205
|
+
|
|
206
|
+
# Request timeout in seconds (default: 300)
|
|
207
|
+
export GFP_MCP_TIMEOUT=300
|
|
208
|
+
|
|
209
|
+
# Enable debug logging (default: false)
|
|
210
|
+
export GFP_MCP_DEBUG=true
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Project Structure
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
gfp-mcp/
|
|
217
|
+
├── mcp_standalone/ # MCP server implementation
|
|
218
|
+
│ ├── __init__.py # Package exports
|
|
219
|
+
│ ├── config.py # Configuration management
|
|
220
|
+
│ ├── client.py # HTTP client for FastAPI
|
|
221
|
+
│ ├── registry.py # Server registry (multi-project support)
|
|
222
|
+
│ ├── tools.py # MCP tool definitions
|
|
223
|
+
│ ├── mappings.py # Tool → Endpoint mappings
|
|
224
|
+
│ ├── server.py # MCP server core
|
|
225
|
+
│ └── README.md # Detailed documentation
|
|
226
|
+
├── tests/ # Test suite
|
|
227
|
+
│ ├── test_mcp_tools.py
|
|
228
|
+
│ ├── test_mcp_mappings.py
|
|
229
|
+
│ ├── test_mcp_integration.py
|
|
230
|
+
│ └── test_registry.py
|
|
231
|
+
├── mcp_serve.py # CLI entry point
|
|
232
|
+
├── MCP_QUICKSTART.md # Quick start guide
|
|
233
|
+
├── MCP_IMPLEMENTATION_STATUS.md # Implementation details
|
|
234
|
+
└── README.md # This file
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Testing
|
|
238
|
+
|
|
239
|
+
Run the test suite:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Run all MCP tests
|
|
243
|
+
pytest tests/test_mcp_*.py -v
|
|
244
|
+
|
|
245
|
+
# Run with coverage
|
|
246
|
+
pytest tests/test_mcp_*.py --cov=mcp_standalone --cov-report=term-missing
|
|
247
|
+
|
|
248
|
+
# Run specific test file
|
|
249
|
+
pytest tests/test_mcp_tools.py -v
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
All tests are passing (46/46 tests):
|
|
253
|
+
- Tool definitions: 15 tests
|
|
254
|
+
- Endpoint mappings: 13 tests
|
|
255
|
+
- Integration & client: 9 tests
|
|
256
|
+
- Registry integration: 9 tests
|
|
257
|
+
|
|
258
|
+
## Example Workflows
|
|
259
|
+
|
|
260
|
+
### Build and Verify a Component
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
User: "List all available photonic components"
|
|
264
|
+
Claude: [Uses list_cells tool]
|
|
265
|
+
|
|
266
|
+
User: "Build the mzi component"
|
|
267
|
+
Claude: [Uses build_cell tool with name="mzi"]
|
|
268
|
+
|
|
269
|
+
User: "Show me the details of the mzi component"
|
|
270
|
+
Claude: [Uses get_cell_info tool with name="mzi"]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Batch Build Multiple Components
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
User: "Build the following components: mzi, coupler, and bend_euler"
|
|
277
|
+
Claude: [Uses build_cells tool with names=["mzi", "coupler", "bend_euler"]]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Troubleshooting
|
|
281
|
+
|
|
282
|
+
### Error: "Connection refused to localhost:8787"
|
|
283
|
+
|
|
284
|
+
**Cause**: FastAPI server is not running
|
|
285
|
+
|
|
286
|
+
**Solution**: Start the FastAPI server:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
gfp serve --port 8787
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Error: "MCP server not responding"
|
|
293
|
+
|
|
294
|
+
**Cause**: STDIO transport issue or MCP client misconfiguration
|
|
295
|
+
|
|
296
|
+
**Solution**:
|
|
297
|
+
1. Check Claude Code/Desktop logs with `claude --debug`
|
|
298
|
+
2. Restart the MCP server
|
|
299
|
+
3. Verify the configuration in settings.json
|
|
300
|
+
|
|
301
|
+
### Error: "Tool execution timeout"
|
|
302
|
+
|
|
303
|
+
**Cause**: Long-running operation exceeded timeout
|
|
304
|
+
|
|
305
|
+
**Solution**: Increase the timeout:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
export GFP_MCP_TIMEOUT=600 # 10 minutes
|
|
309
|
+
gfp mcp-serve
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Roadmap
|
|
313
|
+
|
|
314
|
+
### Future Phases
|
|
315
|
+
|
|
316
|
+
- **Phase 2**: Verification tools (DRC, LVS)
|
|
317
|
+
- **Phase 3**: SPICE workflow tools
|
|
318
|
+
- **Phase 4**: Simulation & advanced tools
|
|
319
|
+
- **Phase 5**: Comprehensive testing & documentation
|
|
320
|
+
|
|
321
|
+
## Documentation
|
|
322
|
+
|
|
323
|
+
- [Quick Start Guide](MCP_QUICKSTART.md) - Step-by-step setup instructions
|
|
324
|
+
- [Implementation Status](MCP_IMPLEMENTATION_STATUS.md) - Detailed implementation notes
|
|
325
|
+
- [MCP Standalone README](mcp_standalone/README.md) - Architecture details
|
|
326
|
+
- [Contributing Guide](CONTRIBUTING.md) - Development and release guidelines
|
|
327
|
+
- [Changelog](CHANGELOG.md) - Version history and release notes
|
|
328
|
+
|
|
329
|
+
## Contributing
|
|
330
|
+
|
|
331
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on:
|
|
332
|
+
|
|
333
|
+
- Development setup
|
|
334
|
+
- Running tests and code quality checks
|
|
335
|
+
- Making changes and submitting PRs
|
|
336
|
+
- Release process (for maintainers)
|
|
337
|
+
|
|
338
|
+
Quick checklist:
|
|
339
|
+
|
|
340
|
+
1. All tests pass: `pytest tests/test_mcp_*.py -v`
|
|
341
|
+
2. Code quality: `ruff check . && ruff format --check .`
|
|
342
|
+
3. Documentation is updated
|
|
343
|
+
4. Changes are backward compatible
|
|
344
|
+
|
|
345
|
+
## License
|
|
346
|
+
|
|
347
|
+
See the main GDSFactory+ project for license information.
|
|
348
|
+
|
|
349
|
+
## Support
|
|
350
|
+
|
|
351
|
+
For issues or questions:
|
|
352
|
+
|
|
353
|
+
1. Check the [Quick Start Guide](MCP_QUICKSTART.md) troubleshooting section
|
|
354
|
+
2. Review the [Implementation Status](MCP_IMPLEMENTATION_STATUS.md) document
|
|
355
|
+
3. Enable debug mode with `GFP_MCP_DEBUG=true` and check server logs
|
|
356
|
+
4. Open an issue on GitHub
|
|
357
|
+
|
|
358
|
+
## Acknowledgments
|
|
359
|
+
|
|
360
|
+
Built on the Model Context Protocol by Anthropic, enabling seamless AI assistant integration with photonic IC design workflows.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
mcp_standalone/__init__.py,sha256=7PhYozHyiRK_oFui0RkF5GJWxA2OZoWVcU_-ESfHIR4,953
|
|
2
|
+
mcp_standalone/client.py,sha256=gUs9m2M27OP-GYmJ9fkojyxv212ETVJFqp0v2uN8Q2c,8205
|
|
3
|
+
mcp_standalone/config.py,sha256=zr2LnI_wNEcMWrnB_WLscR-skkXjE33ZPBtK08XjaX0,1386
|
|
4
|
+
mcp_standalone/mappings.py,sha256=jXEHwSrSYNDuVeMRM2dsSUsr38CzrHBLoe6exkJhqvc,7695
|
|
5
|
+
mcp_standalone/registry.py,sha256=1E61UalVot8HUS3cALjM7ejYB0qR6tI5QbQSZZeQe7Y,6401
|
|
6
|
+
mcp_standalone/server.py,sha256=Celd9j0KY1Fl_Qw09b9Px03vGhtaTUac2ozlYdGJ23A,7100
|
|
7
|
+
mcp_standalone/tools.py,sha256=__j9N396-cJj9CKi6EAdP5_J-xESrbRZG35KNzdszXM,12349
|
|
8
|
+
gfp_mcp-0.1.0.dist-info/METADATA,sha256=dVk1r3JfmlEY3RcqVSDqMLYm2fPAqwYjM4cTUD6sVQE,10442
|
|
9
|
+
gfp_mcp-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
10
|
+
gfp_mcp-0.1.0.dist-info/entry_points.txt,sha256=mgyus9dsB_8mjgnztuHNPqzPi-7HcPg1iYzfM5NMIjk,61
|
|
11
|
+
gfp_mcp-0.1.0.dist-info/top_level.txt,sha256=g2hRJHoDDPNtrNdXR70T7FR9Ev6DTRJiGW7ZvlvnXMc,15
|
|
12
|
+
gfp_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_standalone
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""MCP Standalone Server for GDSFactory+.
|
|
2
|
+
|
|
3
|
+
This package provides a Model Context Protocol (MCP) server that exposes
|
|
4
|
+
GDSFactory+ operations as tools for AI assistants. The server uses STDIO
|
|
5
|
+
transport and proxies requests to the FastAPI backend.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
- Standalone MCP server (this package)
|
|
9
|
+
- STDIO transport for universal compatibility
|
|
10
|
+
- HTTP proxy to FastAPI backend
|
|
11
|
+
- Zero changes to existing FastAPI server
|
|
12
|
+
- No database conflicts (only FastAPI touches SQLite)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from gdsfactoryplus.mcp_standalone import main
|
|
16
|
+
main()
|
|
17
|
+
|
|
18
|
+
Or via CLI:
|
|
19
|
+
gfp mcp-serve
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from .client import FastAPIClient
|
|
25
|
+
from .config import MCPConfig
|
|
26
|
+
from .server import create_server, main, run_server
|
|
27
|
+
from .tools import get_all_tools, get_tool_by_name
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"FastAPIClient",
|
|
31
|
+
"MCPConfig",
|
|
32
|
+
"create_server",
|
|
33
|
+
"main",
|
|
34
|
+
"run_server",
|
|
35
|
+
"get_all_tools",
|
|
36
|
+
"get_tool_by_name",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
mcp_standalone/client.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""HTTP client wrapper for communicating with FastAPI backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 11):
|
|
13
|
+
from typing import Self
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
from .config import MCPConfig
|
|
18
|
+
from .registry import ServerRegistry
|
|
19
|
+
|
|
20
|
+
__all__ = ["FastAPIClient"]
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FastAPIClient:
|
|
26
|
+
"""Async HTTP client for FastAPI backend.
|
|
27
|
+
|
|
28
|
+
Handles connection pooling, retries, and error handling for
|
|
29
|
+
communication with the GDSFactory+ FastAPI server.
|
|
30
|
+
|
|
31
|
+
Supports multi-project routing via the server registry.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, base_url: str | None = None) -> None:
|
|
35
|
+
"""Initialize the FastAPI client.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Base URL for the FastAPI server (default from config)
|
|
39
|
+
If not provided, will use project-based routing via registry
|
|
40
|
+
"""
|
|
41
|
+
self.base_url = base_url or MCPConfig.get_api_url(None)
|
|
42
|
+
self.timeout = MCPConfig.get_timeout()
|
|
43
|
+
self._client: httpx.AsyncClient | None = None
|
|
44
|
+
self._registry = ServerRegistry()
|
|
45
|
+
|
|
46
|
+
async def __aenter__(self) -> Self:
|
|
47
|
+
"""Enter async context."""
|
|
48
|
+
await self.start()
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, *args: object) -> None:
|
|
52
|
+
"""Exit async context."""
|
|
53
|
+
await self.close()
|
|
54
|
+
|
|
55
|
+
async def start(self) -> None:
|
|
56
|
+
"""Start the HTTP client with connection pooling."""
|
|
57
|
+
if self._client is None:
|
|
58
|
+
self._client = httpx.AsyncClient(
|
|
59
|
+
base_url=self.base_url,
|
|
60
|
+
timeout=httpx.Timeout(self.timeout),
|
|
61
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
62
|
+
)
|
|
63
|
+
logger.debug("HTTP client started with base URL: %s", self.base_url)
|
|
64
|
+
|
|
65
|
+
async def close(self) -> None:
|
|
66
|
+
"""Close the HTTP client."""
|
|
67
|
+
if self._client is not None:
|
|
68
|
+
await self._client.aclose()
|
|
69
|
+
self._client = None
|
|
70
|
+
logger.debug("HTTP client closed")
|
|
71
|
+
|
|
72
|
+
def _resolve_base_url(self, project: str | None = None) -> str:
|
|
73
|
+
"""Resolve the base URL for a request.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
project: Optional project name/path to route to specific server
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Base URL for the request
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If project not found in registry
|
|
83
|
+
"""
|
|
84
|
+
# If no project specified, use default base_url
|
|
85
|
+
if project is None:
|
|
86
|
+
return self.base_url
|
|
87
|
+
|
|
88
|
+
# Look up project in registry
|
|
89
|
+
server_info = self._registry.get_server_by_project(project)
|
|
90
|
+
if server_info is None:
|
|
91
|
+
msg = (
|
|
92
|
+
f"Project '{project}' not found in registry. "
|
|
93
|
+
"Make sure the server is running for this project."
|
|
94
|
+
)
|
|
95
|
+
raise ValueError(msg)
|
|
96
|
+
|
|
97
|
+
return f"http://localhost:{server_info.port}"
|
|
98
|
+
|
|
99
|
+
async def request(
|
|
100
|
+
self,
|
|
101
|
+
method: str,
|
|
102
|
+
path: str,
|
|
103
|
+
params: dict[str, Any] | None = None,
|
|
104
|
+
json_data: dict[str, Any] | None = None,
|
|
105
|
+
data: dict[str, Any] | None = None,
|
|
106
|
+
project: str | None = None,
|
|
107
|
+
) -> Any:
|
|
108
|
+
"""Make an HTTP request with retry logic.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
method: HTTP method (GET, POST, etc.)
|
|
112
|
+
path: API endpoint path
|
|
113
|
+
params: Query parameters
|
|
114
|
+
json_data: JSON body data
|
|
115
|
+
data: Form data
|
|
116
|
+
project: Optional project name to route to specific server
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Response data (parsed JSON or raw content)
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
httpx.HTTPError: If request fails after retries
|
|
123
|
+
ValueError: If project not found in registry
|
|
124
|
+
"""
|
|
125
|
+
if self._client is None:
|
|
126
|
+
await self.start()
|
|
127
|
+
|
|
128
|
+
# Resolve the base URL for this request
|
|
129
|
+
base_url = self._resolve_base_url(project)
|
|
130
|
+
|
|
131
|
+
last_error = None
|
|
132
|
+
backoff = MCPConfig.RETRY_BACKOFF
|
|
133
|
+
|
|
134
|
+
for attempt in range(MCPConfig.MAX_RETRIES):
|
|
135
|
+
try:
|
|
136
|
+
logger.debug(
|
|
137
|
+
"Request attempt %d/%d: %s %s (project=%s, base=%s)",
|
|
138
|
+
attempt + 1,
|
|
139
|
+
MCPConfig.MAX_RETRIES,
|
|
140
|
+
method,
|
|
141
|
+
path,
|
|
142
|
+
project or "default",
|
|
143
|
+
base_url,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Build full URL with the resolved base URL
|
|
147
|
+
full_url = f"{base_url}{path}"
|
|
148
|
+
|
|
149
|
+
response = await self._client.request( # type: ignore[union-attr]
|
|
150
|
+
method=method,
|
|
151
|
+
url=full_url,
|
|
152
|
+
params=params,
|
|
153
|
+
json=json_data,
|
|
154
|
+
data=data,
|
|
155
|
+
)
|
|
156
|
+
response.raise_for_status()
|
|
157
|
+
|
|
158
|
+
# Try to parse JSON, fall back to text
|
|
159
|
+
try:
|
|
160
|
+
return response.json()
|
|
161
|
+
except (ValueError, TypeError):
|
|
162
|
+
return response.text
|
|
163
|
+
|
|
164
|
+
except httpx.HTTPError as e:
|
|
165
|
+
last_error = e
|
|
166
|
+
logger.warning("Request failed (attempt %d): %s", attempt + 1, e)
|
|
167
|
+
|
|
168
|
+
# Don't retry on client errors (4xx)
|
|
169
|
+
if (
|
|
170
|
+
isinstance(e, httpx.HTTPStatusError)
|
|
171
|
+
and 400 <= e.response.status_code < 500
|
|
172
|
+
):
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
# Exponential backoff for retries
|
|
176
|
+
if attempt < MCPConfig.MAX_RETRIES - 1:
|
|
177
|
+
await asyncio.sleep(backoff)
|
|
178
|
+
backoff *= 2
|
|
179
|
+
|
|
180
|
+
# All retries failed
|
|
181
|
+
logger.error("All %d attempts failed", MCPConfig.MAX_RETRIES)
|
|
182
|
+
raise last_error # type: ignore[misc]
|
|
183
|
+
|
|
184
|
+
async def get(
|
|
185
|
+
self,
|
|
186
|
+
path: str,
|
|
187
|
+
params: dict[str, Any] | None = None,
|
|
188
|
+
) -> Any:
|
|
189
|
+
"""Make a GET request.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path: API endpoint path
|
|
193
|
+
params: Query parameters
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Response data
|
|
197
|
+
"""
|
|
198
|
+
return await self.request("GET", path, params=params)
|
|
199
|
+
|
|
200
|
+
async def post(
|
|
201
|
+
self,
|
|
202
|
+
path: str,
|
|
203
|
+
json_data: dict[str, Any] | None = None,
|
|
204
|
+
data: dict[str, Any] | None = None,
|
|
205
|
+
) -> Any:
|
|
206
|
+
"""Make a POST request.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
path: API endpoint path
|
|
210
|
+
json_data: JSON body data
|
|
211
|
+
data: Form data
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Response data
|
|
215
|
+
"""
|
|
216
|
+
return await self.request("POST", path, json_data=json_data, data=data)
|
|
217
|
+
|
|
218
|
+
async def health_check(self, project: str | None = None) -> bool:
|
|
219
|
+
"""Check if the FastAPI server is reachable.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
project: Optional project name to check specific server
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if server is healthy, False otherwise
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
await self.request("GET", "/health", project=project)
|
|
229
|
+
except Exception:
|
|
230
|
+
logger.exception("FastAPI server health check failed")
|
|
231
|
+
return False
|
|
232
|
+
else:
|
|
233
|
+
base_url = self._resolve_base_url(project)
|
|
234
|
+
logger.info("FastAPI server is healthy at %s", base_url)
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def list_projects(self) -> list[dict[str, Any]]:
|
|
238
|
+
"""List all active projects from the registry.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of project information dictionaries
|
|
242
|
+
"""
|
|
243
|
+
servers = self._registry.list_servers()
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
"project_name": server.project_name,
|
|
247
|
+
"project_path": server.project_path,
|
|
248
|
+
"port": server.port,
|
|
249
|
+
"pid": server.pid,
|
|
250
|
+
"pdk": server.pdk,
|
|
251
|
+
"started_at": server.started_at,
|
|
252
|
+
}
|
|
253
|
+
for server in servers
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
async def get_project_info(self, project: str) -> dict[str, Any]:
|
|
257
|
+
"""Get detailed information about a specific project.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
project: Project name or path
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Project information from the server's /info endpoint
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: If project not found
|
|
267
|
+
"""
|
|
268
|
+
return await self.request("GET", "/info", project=project)
|