fastmcp 2.10.6__py3-none-any.whl → 2.11.1__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.
- fastmcp/cli/cli.py +128 -33
- fastmcp/cli/install/claude_code.py +42 -1
- fastmcp/cli/install/claude_desktop.py +42 -1
- fastmcp/cli/install/cursor.py +42 -1
- fastmcp/cli/install/mcp_json.py +41 -0
- fastmcp/cli/run.py +127 -1
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/auth/oauth.py +68 -99
- fastmcp/client/oauth_callback.py +18 -0
- fastmcp/client/transports.py +69 -15
- fastmcp/contrib/component_manager/example.py +2 -2
- fastmcp/experimental/server/openapi/README.md +266 -0
- fastmcp/experimental/server/openapi/__init__.py +38 -0
- fastmcp/experimental/server/openapi/components.py +348 -0
- fastmcp/experimental/server/openapi/routing.py +132 -0
- fastmcp/experimental/server/openapi/server.py +466 -0
- fastmcp/experimental/utilities/openapi/README.md +239 -0
- fastmcp/experimental/utilities/openapi/__init__.py +68 -0
- fastmcp/experimental/utilities/openapi/director.py +208 -0
- fastmcp/experimental/utilities/openapi/formatters.py +355 -0
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
- fastmcp/experimental/utilities/openapi/models.py +85 -0
- fastmcp/experimental/utilities/openapi/parser.py +618 -0
- fastmcp/experimental/utilities/openapi/schemas.py +538 -0
- fastmcp/mcp_config.py +125 -88
- fastmcp/prompts/prompt.py +11 -1
- fastmcp/resources/resource.py +21 -1
- fastmcp/resources/template.py +20 -1
- fastmcp/server/auth/__init__.py +18 -2
- fastmcp/server/auth/auth.py +225 -7
- fastmcp/server/auth/providers/bearer.py +25 -473
- fastmcp/server/auth/providers/in_memory.py +4 -2
- fastmcp/server/auth/providers/jwt.py +538 -0
- fastmcp/server/auth/providers/workos.py +151 -0
- fastmcp/server/auth/registry.py +52 -0
- fastmcp/server/context.py +107 -26
- fastmcp/server/dependencies.py +9 -2
- fastmcp/server/http.py +48 -57
- fastmcp/server/middleware/middleware.py +3 -23
- fastmcp/server/openapi.py +1 -1
- fastmcp/server/proxy.py +50 -11
- fastmcp/server/server.py +168 -59
- fastmcp/settings.py +73 -6
- fastmcp/tools/tool.py +36 -3
- fastmcp/tools/tool_manager.py +38 -2
- fastmcp/tools/tool_transform.py +112 -3
- fastmcp/utilities/components.py +41 -3
- fastmcp/utilities/json_schema.py +136 -98
- fastmcp/utilities/json_schema_type.py +1 -3
- fastmcp/utilities/mcp_config.py +28 -0
- fastmcp/utilities/openapi.py +243 -57
- fastmcp/utilities/tests.py +54 -6
- fastmcp/utilities/types.py +94 -11
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/METADATA +4 -3
- fastmcp-2.11.1.dist-info/RECORD +108 -0
- fastmcp/server/auth/providers/bearer_env.py +0 -63
- fastmcp/utilities/cache.py +0 -26
- fastmcp-2.10.6.dist-info/RECORD +0 -93
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.6.dist-info → fastmcp-2.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# OpenAPI Server Implementation (New)
|
|
2
|
+
|
|
3
|
+
This directory contains the next-generation FastMCP server implementation for OpenAPI integration, designed to replace the legacy implementation in `/server/openapi.py`.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
The new implementation uses a **stateless request building approach** with `openapi-core` and `RequestDirector`, providing zero-latency startup and robust OpenAPI support optimized for serverless environments.
|
|
8
|
+
|
|
9
|
+
### Core Components
|
|
10
|
+
|
|
11
|
+
1. **`server.py`** - `FastMCPOpenAPI` main server class with RequestDirector integration
|
|
12
|
+
2. **`components.py`** - Simplified component implementations using RequestDirector
|
|
13
|
+
3. **`routing.py`** - Route mapping and component selection logic
|
|
14
|
+
|
|
15
|
+
### Key Architecture Principles
|
|
16
|
+
|
|
17
|
+
#### 1. Stateless Performance
|
|
18
|
+
- **Zero Startup Latency**: No code generation or heavy initialization
|
|
19
|
+
- **RequestDirector**: Stateless HTTP request building using openapi-core
|
|
20
|
+
- **Pre-calculated Schemas**: All complex processing done during parsing
|
|
21
|
+
|
|
22
|
+
#### 2. Unified Implementation
|
|
23
|
+
- **Single Code Path**: All components use RequestDirector consistently
|
|
24
|
+
- **No Fallbacks**: Simplified architecture without hybrid complexity
|
|
25
|
+
- **Performance First**: Optimized for cold starts and serverless deployments
|
|
26
|
+
|
|
27
|
+
#### 3. OpenAPI Compliance
|
|
28
|
+
- **openapi-core Integration**: Leverages proven library for parameter serialization
|
|
29
|
+
- **Full Feature Support**: Complete OpenAPI 3.0/3.1 support including deepObject
|
|
30
|
+
- **Error Handling**: Comprehensive HTTP error mapping to MCP errors
|
|
31
|
+
|
|
32
|
+
## Component Classes
|
|
33
|
+
|
|
34
|
+
### RequestDirector-Based Components
|
|
35
|
+
|
|
36
|
+
#### `OpenAPITool`
|
|
37
|
+
- Executes operations using RequestDirector for HTTP request building
|
|
38
|
+
- Automatic parameter validation and OpenAPI-compliant serialization
|
|
39
|
+
- Built-in error handling and structured response processing
|
|
40
|
+
- **Advantages**: Zero latency, robust, comprehensive OpenAPI support
|
|
41
|
+
|
|
42
|
+
#### `OpenAPIResource` / `OpenAPIResourceTemplate`
|
|
43
|
+
- Provides resource access using RequestDirector
|
|
44
|
+
- Consistent parameter handling across all resource types
|
|
45
|
+
- Support for complex parameter patterns and collision resolution
|
|
46
|
+
- **Advantages**: High performance, simplified architecture, reliable error handling
|
|
47
|
+
|
|
48
|
+
## Server Implementation
|
|
49
|
+
|
|
50
|
+
### `FastMCPOpenAPI` Class
|
|
51
|
+
|
|
52
|
+
The main server class orchestrates the stateless request building approach:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
class FastMCPOpenAPI(FastMCP):
|
|
56
|
+
def __init__(self, openapi_spec: dict, client: httpx.AsyncClient, **kwargs):
|
|
57
|
+
# 1. Parse OpenAPI spec to HTTP routes with pre-calculated schemas
|
|
58
|
+
self._routes = parse_openapi_to_http_routes(openapi_spec)
|
|
59
|
+
|
|
60
|
+
# 2. Initialize RequestDirector with openapi-core Spec
|
|
61
|
+
self._spec = Spec.from_dict(openapi_spec)
|
|
62
|
+
self._director = RequestDirector(self._spec)
|
|
63
|
+
|
|
64
|
+
# 3. Create components using RequestDirector
|
|
65
|
+
self._create_components()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Component Creation Logic
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def _create_tool(self, route: HTTPRoute) -> Tool:
|
|
72
|
+
# All tools use RequestDirector for consistent, high-performance request building
|
|
73
|
+
return OpenAPITool(
|
|
74
|
+
client=self._client,
|
|
75
|
+
route=route,
|
|
76
|
+
director=self._director,
|
|
77
|
+
name=tool_name,
|
|
78
|
+
description=description,
|
|
79
|
+
parameters=flat_param_schema
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Data Flow
|
|
84
|
+
|
|
85
|
+
### Stateless Request Building
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
OpenAPI Spec → HTTPRoute with Pre-calculated Fields → RequestDirector → HTTP Request → Structured Response
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
1. **Spec Parsing**: OpenAPI spec parsed to `HTTPRoute` models with pre-calculated schemas
|
|
92
|
+
2. **RequestDirector Setup**: openapi-core Spec initialized for request building
|
|
93
|
+
3. **Component Creation**: Create components with RequestDirector reference
|
|
94
|
+
4. **Request Building**: RequestDirector builds HTTP request from flat parameters
|
|
95
|
+
5. **Request Execution**: Execute request with httpx client
|
|
96
|
+
6. **Response Processing**: Return structured MCP response
|
|
97
|
+
|
|
98
|
+
## Key Features
|
|
99
|
+
|
|
100
|
+
### 1. Enhanced Parameter Handling
|
|
101
|
+
|
|
102
|
+
#### Parameter Collision Resolution
|
|
103
|
+
- **Automatic Suffixing**: Colliding parameters get location-based suffixes
|
|
104
|
+
- **Example**: `id` in path and body becomes `id__path` and `id`
|
|
105
|
+
- **Transparent**: LLMs see suffixed parameters, implementation routes correctly
|
|
106
|
+
|
|
107
|
+
#### DeepObject Style Support
|
|
108
|
+
- **Native Support**: Generated client handles all deepObject variations
|
|
109
|
+
- **Explode Handling**: Proper support for explode=true/false
|
|
110
|
+
- **Complex Objects**: Nested object serialization works correctly
|
|
111
|
+
|
|
112
|
+
### 2. Robust Error Handling
|
|
113
|
+
|
|
114
|
+
#### HTTP Error Mapping
|
|
115
|
+
- **Status Code Mapping**: HTTP errors mapped to appropriate MCP errors
|
|
116
|
+
- **Structured Responses**: Error details preserved in tool results
|
|
117
|
+
- **Timeout Handling**: Network timeouts handled gracefully
|
|
118
|
+
|
|
119
|
+
#### Request Building Error Handling
|
|
120
|
+
- **Parameter Validation**: Invalid parameters caught during request building
|
|
121
|
+
- **Schema Validation**: openapi-core validates all OpenAPI constraints
|
|
122
|
+
- **Graceful Degradation**: Missing optional parameters handled smoothly
|
|
123
|
+
|
|
124
|
+
### 3. Performance Optimizations
|
|
125
|
+
|
|
126
|
+
#### Efficient Client Reuse
|
|
127
|
+
- **Connection Pooling**: HTTP connections reused across requests
|
|
128
|
+
- **Client Caching**: Generated clients cached for performance
|
|
129
|
+
- **Async Support**: Full async/await throughout
|
|
130
|
+
|
|
131
|
+
#### Request Optimization
|
|
132
|
+
- **Pre-calculated Schemas**: All complex processing done during initialization
|
|
133
|
+
- **Parameter Mapping**: Collision resolution handled upfront
|
|
134
|
+
- **Zero Latency**: No runtime code generation or complex schema processing
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### Server Options
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
server = FastMCPOpenAPI(
|
|
142
|
+
openapi_spec=spec, # Required: OpenAPI specification
|
|
143
|
+
client=httpx_client, # Required: HTTP client instance
|
|
144
|
+
name="API Server", # Optional: Server name
|
|
145
|
+
route_map=custom_routes, # Optional: Custom route mappings
|
|
146
|
+
enable_caching=True, # Optional: Enable response caching
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Route Mapping Customization
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from fastmcp.server.openapi_new.routing import RouteMap
|
|
154
|
+
|
|
155
|
+
custom_routes = RouteMap({
|
|
156
|
+
"GET:/users": "tool", # Force specific operations to be tools
|
|
157
|
+
"GET:/status": "resource", # Force specific operations to be resources
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Testing Strategy
|
|
162
|
+
|
|
163
|
+
### Test Structure
|
|
164
|
+
|
|
165
|
+
Tests are organized by functionality:
|
|
166
|
+
- `test_server.py` - Server integration and RequestDirector behavior
|
|
167
|
+
- `test_parameter_collisions.py` - Parameter collision handling
|
|
168
|
+
- `test_deepobject_style.py` - DeepObject parameter style support
|
|
169
|
+
- `test_openapi_features.py` - General OpenAPI feature compliance
|
|
170
|
+
|
|
171
|
+
### Testing Philosophy
|
|
172
|
+
|
|
173
|
+
1. **Real Integration**: Test with real OpenAPI specs and HTTP clients
|
|
174
|
+
2. **Minimal Mocking**: Only mock external API endpoints
|
|
175
|
+
3. **Behavioral Focus**: Test behavior, not implementation details
|
|
176
|
+
4. **Performance Focus**: Test that initialization is fast and stateless
|
|
177
|
+
|
|
178
|
+
### Example Test Pattern
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
async def test_stateless_request_building():
|
|
182
|
+
"""Test that server works with stateless RequestDirector approach."""
|
|
183
|
+
|
|
184
|
+
# Test server initialization is fast
|
|
185
|
+
start_time = time.time()
|
|
186
|
+
server = FastMCPOpenAPI(spec=valid_spec, client=client)
|
|
187
|
+
init_time = time.time() - start_time
|
|
188
|
+
assert init_time < 0.01 # Should be very fast
|
|
189
|
+
|
|
190
|
+
# Verify RequestDirector functionality
|
|
191
|
+
assert hasattr(server, '_director')
|
|
192
|
+
assert hasattr(server, '_spec')
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Migration Benefits
|
|
196
|
+
|
|
197
|
+
### From Legacy Implementation
|
|
198
|
+
|
|
199
|
+
1. **Eliminated Startup Latency**: Zero code generation overhead (100-200ms improvement)
|
|
200
|
+
2. **Better OpenAPI Compliance**: openapi-core handles all OpenAPI features correctly
|
|
201
|
+
3. **Serverless Friendly**: Perfect for cold-start environments
|
|
202
|
+
4. **Simplified Architecture**: Single RequestDirector approach eliminates complexity
|
|
203
|
+
5. **Enhanced Reliability**: No dynamic code generation failures
|
|
204
|
+
|
|
205
|
+
### Backward Compatibility
|
|
206
|
+
|
|
207
|
+
- **Same Interface**: Public API unchanged from legacy implementation
|
|
208
|
+
- **Performance Improvement**: Significantly faster initialization
|
|
209
|
+
- **No Breaking Changes**: Existing code works without modification
|
|
210
|
+
|
|
211
|
+
## Monitoring and Debugging
|
|
212
|
+
|
|
213
|
+
### Logging
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
# Enable debug logging to see implementation choices
|
|
217
|
+
import logging
|
|
218
|
+
logging.getLogger("fastmcp.server.openapi_new").setLevel(logging.DEBUG)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Key Log Messages
|
|
222
|
+
- **RequestDirector Initialization**: Success/failure of RequestDirector setup
|
|
223
|
+
- **Schema Pre-calculation**: Pre-calculated schema and parameter map status
|
|
224
|
+
- **Request Building**: Parameter mapping and URL construction details
|
|
225
|
+
- **Performance Metrics**: Request timing and error rates
|
|
226
|
+
|
|
227
|
+
### Debugging Common Issues
|
|
228
|
+
|
|
229
|
+
1. **RequestDirector Initialization Fails**
|
|
230
|
+
- Check OpenAPI spec validity with `openapi-core`
|
|
231
|
+
- Verify spec format is correct JSON/YAML
|
|
232
|
+
- Ensure all required OpenAPI fields are present
|
|
233
|
+
|
|
234
|
+
2. **Parameter Issues**
|
|
235
|
+
- Enable debug logging for parameter processing
|
|
236
|
+
- Check for parameter collision warnings
|
|
237
|
+
- Verify OpenAPI spec parameter definitions
|
|
238
|
+
|
|
239
|
+
3. **Performance Issues**
|
|
240
|
+
- Monitor RequestDirector request building timing
|
|
241
|
+
- Check HTTP client configuration
|
|
242
|
+
- Review response processing timing
|
|
243
|
+
|
|
244
|
+
## Future Enhancements
|
|
245
|
+
|
|
246
|
+
### Planned Features
|
|
247
|
+
|
|
248
|
+
1. **Advanced Caching**: Intelligent response caching with TTL
|
|
249
|
+
2. **Streaming Support**: Handle streaming API responses
|
|
250
|
+
3. **Batch Operations**: Optimize multiple operation calls
|
|
251
|
+
4. **Enhanced Monitoring**: Detailed metrics and health checks
|
|
252
|
+
5. **Configuration Management**: Dynamic configuration updates
|
|
253
|
+
|
|
254
|
+
### Performance Improvements
|
|
255
|
+
|
|
256
|
+
1. **Enhanced Schema Caching**: More aggressive schema pre-calculation
|
|
257
|
+
2. **Parallel Processing**: Concurrent operation execution
|
|
258
|
+
3. **Memory Optimization**: Further reduce memory footprint
|
|
259
|
+
4. **Request Optimization**: Smart request batching and deduplication
|
|
260
|
+
|
|
261
|
+
## Related Documentation
|
|
262
|
+
|
|
263
|
+
- `/utilities/openapi_new/README.md` - Utility implementation details
|
|
264
|
+
- `/server/openapi/README.md` - Legacy implementation reference
|
|
265
|
+
- `/tests/server/openapi_new/` - Comprehensive test suite
|
|
266
|
+
- Project documentation on OpenAPI integration patterns
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""OpenAPI server implementation for FastMCP - refactored for better maintainability."""
|
|
2
|
+
|
|
3
|
+
# Import from server
|
|
4
|
+
from .server import FastMCPOpenAPI
|
|
5
|
+
|
|
6
|
+
# Import from routing
|
|
7
|
+
from .routing import (
|
|
8
|
+
MCPType,
|
|
9
|
+
RouteMap,
|
|
10
|
+
RouteMapFn,
|
|
11
|
+
ComponentFn,
|
|
12
|
+
DEFAULT_ROUTE_MAPPINGS,
|
|
13
|
+
_determine_route_type,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Import from components
|
|
17
|
+
from .components import (
|
|
18
|
+
OpenAPITool,
|
|
19
|
+
OpenAPIResource,
|
|
20
|
+
OpenAPIResourceTemplate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Export public symbols - maintaining backward compatibility
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Server
|
|
26
|
+
"FastMCPOpenAPI",
|
|
27
|
+
# Routing
|
|
28
|
+
"MCPType",
|
|
29
|
+
"RouteMap",
|
|
30
|
+
"RouteMapFn",
|
|
31
|
+
"ComponentFn",
|
|
32
|
+
"DEFAULT_ROUTE_MAPPINGS",
|
|
33
|
+
"_determine_route_type",
|
|
34
|
+
# Components
|
|
35
|
+
"OpenAPITool",
|
|
36
|
+
"OpenAPIResource",
|
|
37
|
+
"OpenAPIResourceTemplate",
|
|
38
|
+
]
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""OpenAPI component implementations: Tool, Resource, and ResourceTemplate classes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from mcp.types import ToolAnnotations
|
|
10
|
+
from pydantic.networks import AnyUrl
|
|
11
|
+
|
|
12
|
+
# Import from our new utilities
|
|
13
|
+
from fastmcp.experimental.utilities.openapi import HTTPRoute
|
|
14
|
+
from fastmcp.experimental.utilities.openapi.director import RequestDirector
|
|
15
|
+
from fastmcp.resources import Resource, ResourceTemplate
|
|
16
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
17
|
+
from fastmcp.tools.tool import Tool, ToolResult
|
|
18
|
+
from fastmcp.utilities.logging import get_logger
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from fastmcp.server import Context
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenAPITool(Tool):
|
|
27
|
+
"""Tool implementation for OpenAPI endpoints."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: httpx.AsyncClient,
|
|
32
|
+
route: HTTPRoute,
|
|
33
|
+
director: RequestDirector,
|
|
34
|
+
name: str,
|
|
35
|
+
description: str,
|
|
36
|
+
parameters: dict[str, Any],
|
|
37
|
+
output_schema: dict[str, Any] | None = None,
|
|
38
|
+
tags: set[str] | None = None,
|
|
39
|
+
timeout: float | None = None,
|
|
40
|
+
annotations: ToolAnnotations | None = None,
|
|
41
|
+
serializer: Callable[[Any], str] | None = None,
|
|
42
|
+
):
|
|
43
|
+
super().__init__(
|
|
44
|
+
name=name,
|
|
45
|
+
description=description,
|
|
46
|
+
parameters=parameters,
|
|
47
|
+
output_schema=output_schema,
|
|
48
|
+
tags=tags or set(),
|
|
49
|
+
annotations=annotations,
|
|
50
|
+
serializer=serializer,
|
|
51
|
+
)
|
|
52
|
+
self._client = client
|
|
53
|
+
self._route = route
|
|
54
|
+
self._director = director
|
|
55
|
+
self._timeout = timeout
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
59
|
+
return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
|
|
60
|
+
|
|
61
|
+
async def run(self, arguments: dict[str, Any]) -> ToolResult:
|
|
62
|
+
"""Execute the HTTP request using RequestDirector for simplified parameter handling."""
|
|
63
|
+
try:
|
|
64
|
+
# Get base URL from client
|
|
65
|
+
base_url = (
|
|
66
|
+
str(self._client.base_url)
|
|
67
|
+
if hasattr(self._client, "base_url") and self._client.base_url
|
|
68
|
+
else "http://localhost"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Get Headers from client
|
|
72
|
+
cli_headers = (
|
|
73
|
+
self._client.headers
|
|
74
|
+
if hasattr(self._client, "headers") and self._client.headers
|
|
75
|
+
else {}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Build the request using RequestDirector
|
|
79
|
+
request = self._director.build(self._route, arguments, base_url)
|
|
80
|
+
|
|
81
|
+
# First add server headers (lowest precedence)
|
|
82
|
+
if cli_headers:
|
|
83
|
+
# Merge with existing headers, _client headers as base
|
|
84
|
+
if request.headers:
|
|
85
|
+
# Start with request headers, then add client headers
|
|
86
|
+
for key, value in cli_headers.items():
|
|
87
|
+
if key not in request.headers:
|
|
88
|
+
request.headers[key] = value
|
|
89
|
+
else:
|
|
90
|
+
# Create new headers from cli_headers
|
|
91
|
+
for key, value in cli_headers.items():
|
|
92
|
+
request.headers[key] = value
|
|
93
|
+
|
|
94
|
+
# Then add MCP client transport headers (highest precedence)
|
|
95
|
+
mcp_headers = get_http_headers()
|
|
96
|
+
if mcp_headers:
|
|
97
|
+
# Merge with existing headers, MCP headers take precedence over all
|
|
98
|
+
if request.headers:
|
|
99
|
+
request.headers.update(mcp_headers)
|
|
100
|
+
else:
|
|
101
|
+
# Create new headers from mcp_headers
|
|
102
|
+
for key, value in mcp_headers.items():
|
|
103
|
+
request.headers[key] = value
|
|
104
|
+
# print logger
|
|
105
|
+
logger.debug(f"run - sending request; headers: {request.headers}")
|
|
106
|
+
|
|
107
|
+
# Execute the request
|
|
108
|
+
# Note: httpx.AsyncClient.send() doesn't accept timeout parameter
|
|
109
|
+
# The timeout should be configured on the client itself
|
|
110
|
+
response = await self._client.send(request)
|
|
111
|
+
|
|
112
|
+
# Raise for 4xx/5xx responses
|
|
113
|
+
response.raise_for_status()
|
|
114
|
+
|
|
115
|
+
# Try to parse as JSON first
|
|
116
|
+
try:
|
|
117
|
+
result = response.json()
|
|
118
|
+
|
|
119
|
+
# Handle structured content based on output schema, if any
|
|
120
|
+
structured_output = None
|
|
121
|
+
if self.output_schema is not None:
|
|
122
|
+
if self.output_schema.get("x-fastmcp-wrap-result"):
|
|
123
|
+
# Schema says wrap - always wrap in result key
|
|
124
|
+
structured_output = {"result": result}
|
|
125
|
+
else:
|
|
126
|
+
structured_output = result
|
|
127
|
+
# If no output schema, use fallback logic for backward compatibility
|
|
128
|
+
elif not isinstance(result, dict):
|
|
129
|
+
structured_output = {"result": result}
|
|
130
|
+
else:
|
|
131
|
+
structured_output = result
|
|
132
|
+
|
|
133
|
+
return ToolResult(structured_content=structured_output)
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
return ToolResult(content=response.text)
|
|
136
|
+
|
|
137
|
+
except httpx.HTTPStatusError as e:
|
|
138
|
+
# Handle HTTP errors (4xx, 5xx)
|
|
139
|
+
error_message = (
|
|
140
|
+
f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
|
|
141
|
+
)
|
|
142
|
+
try:
|
|
143
|
+
error_data = e.response.json()
|
|
144
|
+
error_message += f" - {error_data}"
|
|
145
|
+
except (json.JSONDecodeError, ValueError):
|
|
146
|
+
if e.response.text:
|
|
147
|
+
error_message += f" - {e.response.text}"
|
|
148
|
+
|
|
149
|
+
raise ValueError(error_message)
|
|
150
|
+
|
|
151
|
+
except httpx.RequestError as e:
|
|
152
|
+
# Handle request errors (connection, timeout, etc.)
|
|
153
|
+
raise ValueError(f"Request error: {str(e)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class OpenAPIResource(Resource):
|
|
157
|
+
"""Resource implementation for OpenAPI endpoints."""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
client: httpx.AsyncClient,
|
|
162
|
+
route: HTTPRoute,
|
|
163
|
+
director: RequestDirector,
|
|
164
|
+
uri: str,
|
|
165
|
+
name: str,
|
|
166
|
+
description: str,
|
|
167
|
+
mime_type: str = "application/json",
|
|
168
|
+
tags: set[str] = set(),
|
|
169
|
+
timeout: float | None = None,
|
|
170
|
+
):
|
|
171
|
+
super().__init__(
|
|
172
|
+
uri=AnyUrl(uri), # Convert string to AnyUrl
|
|
173
|
+
name=name,
|
|
174
|
+
description=description,
|
|
175
|
+
mime_type=mime_type,
|
|
176
|
+
tags=tags,
|
|
177
|
+
)
|
|
178
|
+
self._client = client
|
|
179
|
+
self._route = route
|
|
180
|
+
self._director = director
|
|
181
|
+
self._timeout = timeout
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
185
|
+
return f"OpenAPIResource(name={self.name!r}, uri={self.uri!r}, path={self._route.path})"
|
|
186
|
+
|
|
187
|
+
async def read(self) -> str | bytes:
|
|
188
|
+
"""Fetch the resource data by making an HTTP request."""
|
|
189
|
+
try:
|
|
190
|
+
# Extract path parameters from the URI if present
|
|
191
|
+
path = self._route.path
|
|
192
|
+
resource_uri = str(self.uri)
|
|
193
|
+
|
|
194
|
+
# If this is a templated resource, extract path parameters from the URI
|
|
195
|
+
if "{" in path and "}" in path:
|
|
196
|
+
# Extract the resource ID from the URI (the last part after the last slash)
|
|
197
|
+
parts = resource_uri.split("/")
|
|
198
|
+
|
|
199
|
+
if len(parts) > 1:
|
|
200
|
+
# Find all path parameters in the route path
|
|
201
|
+
path_params = {}
|
|
202
|
+
|
|
203
|
+
# Find the path parameter names from the route path
|
|
204
|
+
param_matches = re.findall(r"\{([^}]+)\}", path)
|
|
205
|
+
if param_matches:
|
|
206
|
+
# Reverse sorting from creation order (traversal is backwards)
|
|
207
|
+
param_matches.sort(reverse=True)
|
|
208
|
+
# Number of sent parameters is number of parts -1 (assuming first part is resource identifier)
|
|
209
|
+
expected_param_count = len(parts) - 1
|
|
210
|
+
# Map parameters from the end of the URI to the parameters in the path
|
|
211
|
+
# Last parameter in URI (parts[-1]) maps to last parameter in path, and so on
|
|
212
|
+
for i, param_name in enumerate(param_matches):
|
|
213
|
+
# Ensure we don't use resource identifier as parameter
|
|
214
|
+
if i < expected_param_count:
|
|
215
|
+
# Get values from the end of parts
|
|
216
|
+
param_value = parts[-1 - i]
|
|
217
|
+
path_params[param_name] = param_value
|
|
218
|
+
|
|
219
|
+
# Replace path parameters with their values
|
|
220
|
+
for param_name, param_value in path_params.items():
|
|
221
|
+
path = path.replace(f"{{{param_name}}}", str(param_value))
|
|
222
|
+
|
|
223
|
+
# Filter any query parameters - get query parameters and filter out None/empty values
|
|
224
|
+
query_params = {}
|
|
225
|
+
for param in self._route.parameters:
|
|
226
|
+
if param.location == "query" and hasattr(self, f"_{param.name}"):
|
|
227
|
+
value = getattr(self, f"_{param.name}")
|
|
228
|
+
if value is not None and value != "":
|
|
229
|
+
query_params[param.name] = value
|
|
230
|
+
|
|
231
|
+
# Prepare headers with correct precedence: server < client transport
|
|
232
|
+
headers = {}
|
|
233
|
+
# Start with server headers (lowest precedence)
|
|
234
|
+
cli_headers = (
|
|
235
|
+
self._client.headers
|
|
236
|
+
if hasattr(self._client, "headers") and self._client.headers
|
|
237
|
+
else {}
|
|
238
|
+
)
|
|
239
|
+
headers.update(cli_headers)
|
|
240
|
+
|
|
241
|
+
# Add MCP client transport headers (highest precedence)
|
|
242
|
+
mcp_headers = get_http_headers()
|
|
243
|
+
headers.update(mcp_headers)
|
|
244
|
+
|
|
245
|
+
response = await self._client.request(
|
|
246
|
+
method=self._route.method,
|
|
247
|
+
url=path,
|
|
248
|
+
params=query_params,
|
|
249
|
+
headers=headers,
|
|
250
|
+
timeout=self._timeout,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Raise for 4xx/5xx responses
|
|
254
|
+
response.raise_for_status()
|
|
255
|
+
|
|
256
|
+
# Determine content type and return appropriate format
|
|
257
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
258
|
+
|
|
259
|
+
if "application/json" in content_type:
|
|
260
|
+
result = response.json()
|
|
261
|
+
return json.dumps(result)
|
|
262
|
+
elif any(ct in content_type for ct in ["text/", "application/xml"]):
|
|
263
|
+
return response.text
|
|
264
|
+
else:
|
|
265
|
+
return response.content
|
|
266
|
+
|
|
267
|
+
except httpx.HTTPStatusError as e:
|
|
268
|
+
# Handle HTTP errors (4xx, 5xx)
|
|
269
|
+
error_message = (
|
|
270
|
+
f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
error_data = e.response.json()
|
|
274
|
+
error_message += f" - {error_data}"
|
|
275
|
+
except (json.JSONDecodeError, ValueError):
|
|
276
|
+
if e.response.text:
|
|
277
|
+
error_message += f" - {e.response.text}"
|
|
278
|
+
|
|
279
|
+
raise ValueError(error_message)
|
|
280
|
+
|
|
281
|
+
except httpx.RequestError as e:
|
|
282
|
+
# Handle request errors (connection, timeout, etc.)
|
|
283
|
+
raise ValueError(f"Request error: {str(e)}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class OpenAPIResourceTemplate(ResourceTemplate):
|
|
287
|
+
"""Resource template implementation for OpenAPI endpoints."""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
client: httpx.AsyncClient,
|
|
292
|
+
route: HTTPRoute,
|
|
293
|
+
director: RequestDirector,
|
|
294
|
+
uri_template: str,
|
|
295
|
+
name: str,
|
|
296
|
+
description: str,
|
|
297
|
+
parameters: dict[str, Any],
|
|
298
|
+
tags: set[str] = set(),
|
|
299
|
+
timeout: float | None = None,
|
|
300
|
+
):
|
|
301
|
+
super().__init__(
|
|
302
|
+
uri_template=uri_template,
|
|
303
|
+
name=name,
|
|
304
|
+
description=description,
|
|
305
|
+
parameters=parameters,
|
|
306
|
+
tags=tags,
|
|
307
|
+
)
|
|
308
|
+
self._client = client
|
|
309
|
+
self._route = route
|
|
310
|
+
self._director = director
|
|
311
|
+
self._timeout = timeout
|
|
312
|
+
|
|
313
|
+
def __repr__(self) -> str:
|
|
314
|
+
"""Custom representation to prevent recursion errors when printing."""
|
|
315
|
+
return f"OpenAPIResourceTemplate(name={self.name!r}, uri_template={self.uri_template!r}, path={self._route.path})"
|
|
316
|
+
|
|
317
|
+
async def create_resource(
|
|
318
|
+
self,
|
|
319
|
+
uri: str,
|
|
320
|
+
params: dict[str, Any],
|
|
321
|
+
context: "Context | None" = None,
|
|
322
|
+
) -> Resource:
|
|
323
|
+
"""Create a resource with the given parameters."""
|
|
324
|
+
# Generate a URI for this resource instance
|
|
325
|
+
uri_parts = []
|
|
326
|
+
for key, value in params.items():
|
|
327
|
+
uri_parts.append(f"{key}={value}")
|
|
328
|
+
|
|
329
|
+
# Create and return a resource
|
|
330
|
+
return OpenAPIResource(
|
|
331
|
+
client=self._client,
|
|
332
|
+
route=self._route,
|
|
333
|
+
director=self._director,
|
|
334
|
+
uri=uri,
|
|
335
|
+
name=f"{self.name}-{'-'.join(uri_parts)}",
|
|
336
|
+
description=self.description or f"Resource for {self._route.path}",
|
|
337
|
+
mime_type="application/json",
|
|
338
|
+
tags=set(self._route.tags or []),
|
|
339
|
+
timeout=self._timeout,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Export public symbols
|
|
344
|
+
__all__ = [
|
|
345
|
+
"OpenAPITool",
|
|
346
|
+
"OpenAPIResource",
|
|
347
|
+
"OpenAPIResourceTemplate",
|
|
348
|
+
]
|