fastmcp 2.9.2__py3-none-any.whl → 2.10.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.
Files changed (39) hide show
  1. fastmcp/client/auth/oauth.py +5 -82
  2. fastmcp/client/client.py +114 -24
  3. fastmcp/client/elicitation.py +63 -0
  4. fastmcp/client/transports.py +50 -36
  5. fastmcp/contrib/component_manager/README.md +170 -0
  6. fastmcp/contrib/component_manager/__init__.py +4 -0
  7. fastmcp/contrib/component_manager/component_manager.py +186 -0
  8. fastmcp/contrib/component_manager/component_service.py +225 -0
  9. fastmcp/contrib/component_manager/example.py +59 -0
  10. fastmcp/prompts/prompt.py +12 -4
  11. fastmcp/resources/resource.py +8 -3
  12. fastmcp/resources/template.py +5 -0
  13. fastmcp/server/auth/auth.py +15 -0
  14. fastmcp/server/auth/providers/bearer.py +41 -3
  15. fastmcp/server/auth/providers/bearer_env.py +4 -0
  16. fastmcp/server/auth/providers/in_memory.py +15 -0
  17. fastmcp/server/context.py +144 -4
  18. fastmcp/server/elicitation.py +160 -0
  19. fastmcp/server/http.py +1 -9
  20. fastmcp/server/low_level.py +4 -2
  21. fastmcp/server/middleware/__init__.py +14 -1
  22. fastmcp/server/middleware/logging.py +11 -0
  23. fastmcp/server/middleware/middleware.py +10 -6
  24. fastmcp/server/openapi.py +19 -77
  25. fastmcp/server/proxy.py +13 -6
  26. fastmcp/server/server.py +27 -7
  27. fastmcp/settings.py +0 -17
  28. fastmcp/tools/tool.py +209 -57
  29. fastmcp/tools/tool_manager.py +2 -3
  30. fastmcp/tools/tool_transform.py +125 -26
  31. fastmcp/utilities/components.py +5 -1
  32. fastmcp/utilities/json_schema_type.py +648 -0
  33. fastmcp/utilities/openapi.py +69 -0
  34. fastmcp/utilities/types.py +50 -19
  35. {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/METADATA +3 -2
  36. {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/RECORD +39 -31
  37. {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/WHEEL +0 -0
  38. {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/entry_points.txt +0 -0
  39. {fastmcp-2.9.2.dist-info → fastmcp-2.10.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Generic, Literal, TypeVar
5
+
6
+ from mcp.server.elicitation import (
7
+ CancelledElicitation,
8
+ DeclinedElicitation,
9
+ )
10
+ from pydantic import BaseModel
11
+
12
+ from fastmcp.utilities.json_schema import compress_schema
13
+ from fastmcp.utilities.logging import get_logger
14
+ from fastmcp.utilities.types import get_cached_typeadapter
15
+
16
+ __all__ = [
17
+ "AcceptedElicitation",
18
+ "CancelledElicitation",
19
+ "DeclinedElicitation",
20
+ "get_elicitation_schema",
21
+ "ScalarElicitationType",
22
+ ]
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ # we can't use the low-level AcceptedElicitation because it only works with BaseModels
30
+ class AcceptedElicitation(BaseModel, Generic[T]):
31
+ """Result when user accepts the elicitation."""
32
+
33
+ action: Literal["accept"] = "accept"
34
+ data: T
35
+
36
+
37
+ @dataclass
38
+ class ScalarElicitationType(Generic[T]):
39
+ value: T
40
+
41
+
42
+ def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
43
+ """Get the schema for an elicitation response.
44
+
45
+ Args:
46
+ response_type: The type of the response
47
+ """
48
+
49
+ schema = get_cached_typeadapter(response_type).json_schema()
50
+ schema = compress_schema(schema)
51
+
52
+ # Validate the schema to ensure it follows MCP elicitation requirements
53
+ validate_elicitation_json_schema(schema)
54
+
55
+ return schema
56
+
57
+
58
+ def validate_elicitation_json_schema(schema: dict[str, Any]) -> None:
59
+ """Validate that a JSON schema follows MCP elicitation requirements.
60
+
61
+ This ensures the schema is compatible with MCP elicitation requirements:
62
+ - Must be an object schema
63
+ - Must only contain primitive field types (string, number, integer, boolean)
64
+ - Must be flat (no nested objects or arrays of objects)
65
+ - Allows const fields (for Literal types) and enum fields (for Enum types)
66
+ - Only primitive types and their nullable variants are allowed
67
+
68
+ Args:
69
+ schema: The JSON schema to validate
70
+
71
+ Raises:
72
+ TypeError: If the schema doesn't meet MCP elicitation requirements
73
+ """
74
+ ALLOWED_TYPES = {"string", "number", "integer", "boolean"}
75
+
76
+ # Check that the schema is an object
77
+ if schema.get("type") != "object":
78
+ raise TypeError(
79
+ f"Elicitation schema must be an object schema, got type '{schema.get('type')}'. "
80
+ "Elicitation schemas are limited to flat objects with primitive properties only."
81
+ )
82
+
83
+ properties = schema.get("properties", {})
84
+
85
+ for prop_name, prop_schema in properties.items():
86
+ prop_type = prop_schema.get("type")
87
+
88
+ # Handle nullable types
89
+ if isinstance(prop_type, list):
90
+ if "null" in prop_type:
91
+ prop_type = [t for t in prop_type if t != "null"]
92
+ if len(prop_type) == 1:
93
+ prop_type = prop_type[0]
94
+ elif prop_schema.get("nullable", False):
95
+ continue # Nullable with no other type is fine
96
+
97
+ # Handle const fields (Literal types)
98
+ if "const" in prop_schema:
99
+ continue # const fields are allowed regardless of type
100
+
101
+ # Handle enum fields (Enum types)
102
+ if "enum" in prop_schema:
103
+ continue # enum fields are allowed regardless of type
104
+
105
+ # Handle references to definitions (like Enum types)
106
+ if "$ref" in prop_schema:
107
+ # Get the referenced definition
108
+ ref_path = prop_schema["$ref"]
109
+ if ref_path.startswith("#/$defs/"):
110
+ def_name = ref_path[8:] # Remove "#/$defs/" prefix
111
+ ref_def = schema.get("$defs", {}).get(def_name, {})
112
+ # If the referenced definition has an enum, it's allowed
113
+ if "enum" in ref_def:
114
+ continue
115
+ # If the referenced definition has a type that's allowed, it's allowed
116
+ ref_type = ref_def.get("type")
117
+ if ref_type in ALLOWED_TYPES:
118
+ continue
119
+ # If we can't determine what the ref points to, reject it for safety
120
+ raise TypeError(
121
+ f"Elicitation schema field '{prop_name}' contains a reference '{ref_path}' "
122
+ "that could not be validated. Only references to enum types or primitive types are allowed."
123
+ )
124
+
125
+ # Handle union types (oneOf/anyOf)
126
+ if "oneOf" in prop_schema or "anyOf" in prop_schema:
127
+ union_schemas = prop_schema.get("oneOf", []) + prop_schema.get("anyOf", [])
128
+ for union_schema in union_schemas:
129
+ # Allow const and enum in unions
130
+ if "const" in union_schema or "enum" in union_schema:
131
+ continue
132
+ union_type = union_schema.get("type")
133
+ if union_type not in ALLOWED_TYPES:
134
+ raise TypeError(
135
+ f"Elicitation schema field '{prop_name}' has union type '{union_type}' which is not "
136
+ f"a primitive type. Only {ALLOWED_TYPES} are allowed in elicitation schemas."
137
+ )
138
+ continue
139
+
140
+ # Check if it's a primitive type
141
+ if prop_type not in ALLOWED_TYPES:
142
+ raise TypeError(
143
+ f"Elicitation schema field '{prop_name}' has type '{prop_type}' which is not "
144
+ f"a primitive type. Only {ALLOWED_TYPES} are allowed in elicitation schemas."
145
+ )
146
+
147
+ # Check for nested objects or arrays of objects (not allowed)
148
+ if prop_type == "object":
149
+ raise TypeError(
150
+ f"Elicitation schema field '{prop_name}' is an object, but nested objects are not allowed. "
151
+ "Elicitation schemas must be flat objects with primitive properties only."
152
+ )
153
+
154
+ if prop_type == "array":
155
+ items_schema = prop_schema.get("items", {})
156
+ if items_schema.get("type") == "object":
157
+ raise TypeError(
158
+ f"Elicitation schema field '{prop_name}' is an array of objects, but arrays of objects are not allowed. "
159
+ "Elicitation schemas must be flat objects with primitive properties only."
160
+ )
fastmcp/server/http.py CHANGED
@@ -87,7 +87,7 @@ def setup_auth_middleware_and_routes(
87
87
  middleware = [
88
88
  Middleware(
89
89
  AuthenticationMiddleware,
90
- backend=BearerAuthBackend(provider=auth),
90
+ backend=BearerAuthBackend(auth),
91
91
  ),
92
92
  Middleware(AuthContextMiddleware),
93
93
  ]
@@ -158,10 +158,6 @@ def create_sse_app(
158
158
  A Starlette application with RequestContextMiddleware
159
159
  """
160
160
 
161
- # Ensure the message_path ends with a trailing slash to avoid automatic redirects
162
- if not message_path.endswith("/"):
163
- message_path = message_path + "/"
164
-
165
161
  server_routes: list[BaseRoute] = []
166
162
  server_middleware: list[Middleware] = []
167
163
 
@@ -309,10 +305,6 @@ def create_streamable_http_app(
309
305
  # Re-raise other RuntimeErrors if they don't match the specific message
310
306
  raise
311
307
 
312
- # Ensure the streamable_http_path ends with a trailing slash to avoid automatic redirects
313
- if not streamable_http_path.endswith("/"):
314
- streamable_http_path = streamable_http_path + "/"
315
-
316
308
  # Add StreamableHTTP routes with or without auth
317
309
  if auth:
318
310
  auth_middleware, auth_routes, required_scopes = (
@@ -4,12 +4,14 @@ from mcp.server.lowlevel.server import (
4
4
  LifespanResultT,
5
5
  NotificationOptions,
6
6
  RequestT,
7
- Server,
7
+ )
8
+ from mcp.server.lowlevel.server import (
9
+ Server as _Server,
8
10
  )
9
11
  from mcp.server.models import InitializationOptions
10
12
 
11
13
 
12
- class LowLevelServer(Server[LifespanResultT, RequestT]):
14
+ class LowLevelServer(_Server[LifespanResultT, RequestT]):
13
15
  def __init__(self, *args, **kwargs):
14
16
  super().__init__(*args, **kwargs)
15
17
  # FastMCP servers support notifications for all components
@@ -1,6 +1,19 @@
1
- from .middleware import Middleware, MiddlewareContext
1
+ from .middleware import (
2
+ Middleware,
3
+ MiddlewareContext,
4
+ CallNext,
5
+ ListToolsResult,
6
+ ListResourcesResult,
7
+ ListResourceTemplatesResult,
8
+ ListPromptsResult,
9
+ )
2
10
 
3
11
  __all__ = [
4
12
  "Middleware",
5
13
  "MiddlewareContext",
14
+ "CallNext",
15
+ "ListToolsResult",
16
+ "ListResourcesResult",
17
+ "ListResourceTemplatesResult",
18
+ "ListPromptsResult",
6
19
  ]
@@ -32,6 +32,7 @@ class LoggingMiddleware(Middleware):
32
32
  log_level: int = logging.INFO,
33
33
  include_payloads: bool = False,
34
34
  max_payload_length: int = 1000,
35
+ methods: list[str] | None = None,
35
36
  ):
36
37
  """Initialize logging middleware.
37
38
 
@@ -40,11 +41,13 @@ class LoggingMiddleware(Middleware):
40
41
  log_level: Log level for messages (default: INFO)
41
42
  include_payloads: Whether to include message payloads in logs
42
43
  max_payload_length: Maximum length of payload to log (prevents huge logs)
44
+ methods: List of methods to log. If None, logs all methods.
43
45
  """
44
46
  self.logger = logger or logging.getLogger("fastmcp.requests")
45
47
  self.log_level = log_level
46
48
  self.include_payloads = include_payloads
47
49
  self.max_payload_length = max_payload_length
50
+ self.methods = methods
48
51
 
49
52
  def _format_message(self, context: MiddlewareContext) -> str:
50
53
  """Format a message for logging."""
@@ -68,6 +71,8 @@ class LoggingMiddleware(Middleware):
68
71
  async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
69
72
  """Log all messages."""
70
73
  message_info = self._format_message(context)
74
+ if self.methods and context.method not in self.methods:
75
+ return await call_next(context)
71
76
 
72
77
  self.logger.log(self.log_level, f"Processing message: {message_info}")
73
78
 
@@ -105,6 +110,7 @@ class StructuredLoggingMiddleware(Middleware):
105
110
  logger: logging.Logger | None = None,
106
111
  log_level: int = logging.INFO,
107
112
  include_payloads: bool = False,
113
+ methods: list[str] | None = None,
108
114
  ):
109
115
  """Initialize structured logging middleware.
110
116
 
@@ -112,10 +118,12 @@ class StructuredLoggingMiddleware(Middleware):
112
118
  logger: Logger instance to use. If None, creates a logger named 'fastmcp.structured'
113
119
  log_level: Log level for messages (default: INFO)
114
120
  include_payloads: Whether to include message payloads in logs
121
+ methods: List of methods to log. If None, logs all methods.
115
122
  """
116
123
  self.logger = logger or logging.getLogger("fastmcp.structured")
117
124
  self.log_level = log_level
118
125
  self.include_payloads = include_payloads
126
+ self.methods = methods
119
127
 
120
128
  def _create_log_entry(
121
129
  self, context: MiddlewareContext, event: str, **extra_fields
@@ -141,6 +149,9 @@ class StructuredLoggingMiddleware(Middleware):
141
149
  async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
142
150
  """Log structured message information."""
143
151
  start_entry = self._create_log_entry(context, "request_start")
152
+ if self.methods and context.method not in self.methods:
153
+ return await call_next(context)
154
+
144
155
  self.logger.log(self.log_level, json.dumps(start_entry))
145
156
 
146
157
  try:
@@ -25,6 +25,16 @@ from fastmcp.tools.tool import Tool
25
25
  if TYPE_CHECKING:
26
26
  from fastmcp.server.context import Context
27
27
 
28
+ __all__ = [
29
+ "Middleware",
30
+ "MiddlewareContext",
31
+ "CallNext",
32
+ "ListToolsResult",
33
+ "ListResourcesResult",
34
+ "ListResourceTemplatesResult",
35
+ "ListPromptsResult",
36
+ ]
37
+
28
38
  logger = logging.getLogger(__name__)
29
39
 
30
40
 
@@ -52,12 +62,6 @@ ServerResultT = TypeVar(
52
62
  )
53
63
 
54
64
 
55
- @dataclass(kw_only=True)
56
- class CallToolResult:
57
- content: list[mt.Content]
58
- isError: bool = False
59
-
60
-
61
65
  @dataclass(kw_only=True)
62
66
  class ListToolsResult:
63
67
  tools: dict[str, Tool]
fastmcp/server/openapi.py CHANGED
@@ -21,15 +21,15 @@ from fastmcp.exceptions import ToolError
21
21
  from fastmcp.resources import Resource, ResourceTemplate
22
22
  from fastmcp.server.dependencies import get_http_headers
23
23
  from fastmcp.server.server import FastMCP
24
- from fastmcp.tools.tool import Tool, _convert_to_content
24
+ from fastmcp.tools.tool import Tool, ToolResult
25
25
  from fastmcp.utilities import openapi
26
26
  from fastmcp.utilities.logging import get_logger
27
27
  from fastmcp.utilities.openapi import (
28
28
  HTTPRoute,
29
29
  _combine_schemas,
30
+ format_array_parameter,
30
31
  format_description_with_responses,
31
32
  )
32
- from fastmcp.utilities.types import MCPContent
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  from fastmcp.server import Context
@@ -255,7 +255,7 @@ class OpenAPITool(Tool):
255
255
  """Custom representation to prevent recursion errors when printing."""
256
256
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
257
257
 
258
- async def run(self, arguments: dict[str, Any]) -> list[MCPContent]:
258
+ async def run(self, arguments: dict[str, Any]) -> ToolResult:
259
259
  """Execute the HTTP request based on the route configuration."""
260
260
 
261
261
  # Prepare URL
@@ -297,46 +297,10 @@ class OpenAPITool(Tool):
297
297
  if is_array:
298
298
  # Format array values as comma-separated string
299
299
  # This follows the OpenAPI 'simple' style (default for path)
300
- if all(
301
- isinstance(item, str | int | float | bool)
302
- for item in param_value
303
- ):
304
- # Handle simple array types
305
- path = path.replace(
306
- f"{{{param_name}}}", ",".join(str(v) for v in param_value)
307
- )
308
- else:
309
- # Handle complex array types (containing objects/dicts)
310
- try:
311
- # Try to create a simple representation without Python syntax artifacts
312
- formatted_parts = []
313
- for item in param_value:
314
- if isinstance(item, dict):
315
- # For objects, serialize key-value pairs
316
- item_parts = []
317
- for k, v in item.items():
318
- item_parts.append(f"{k}:{v}")
319
- formatted_parts.append(".".join(item_parts))
320
- else:
321
- # Fallback for other complex types
322
- formatted_parts.append(str(item))
323
-
324
- # Join parts with commas
325
- formatted_value = ",".join(formatted_parts)
326
- path = path.replace(f"{{{param_name}}}", formatted_value)
327
- except Exception as e:
328
- logger.warning(
329
- f"Failed to format complex array path parameter '{param_name}': {e}"
330
- )
331
- # Fallback to string representation, but remove Python syntax artifacts
332
- str_value = (
333
- str(param_value)
334
- .replace("[", "")
335
- .replace("]", "")
336
- .replace("'", "")
337
- .replace('"', "")
338
- )
339
- path = path.replace(f"{{{param_name}}}", str_value)
300
+ formatted_value = format_array_parameter(
301
+ param_value, param_name, is_query_parameter=False
302
+ )
303
+ path = path.replace(f"{{{param_name}}}", str(formatted_value))
340
304
  continue
341
305
 
342
306
  # Default handling for non-array parameters or non-array schemas
@@ -356,44 +320,21 @@ class OpenAPITool(Tool):
356
320
  # Format array query parameters as comma-separated strings
357
321
  # following OpenAPI form style (default for query parameters)
358
322
  if isinstance(param_value, list) and p.schema_.get("type") == "array":
359
- # Get explode parameter from schema, default is True for query parameters
323
+ # Get explode parameter from the parameter info, default is True for query parameters
360
324
  # If explode is True, the array is serialized as separate parameters
361
325
  # If explode is False, the array is serialized as a comma-separated string
362
- explode = p.schema_.get("explode", True)
326
+ explode = p.explode if p.explode is not None else True
363
327
 
364
328
  if explode:
365
329
  # When explode=True, we pass the array directly, which HTTPX will serialize
366
330
  # as multiple parameters with the same name
367
331
  query_params[p.name] = param_value
368
332
  else:
369
- # For arrays of simple types (strings, numbers, etc.), join with commas
370
- if all(
371
- isinstance(item, str | int | float | bool)
372
- for item in param_value
373
- ):
374
- query_params[p.name] = ",".join(str(v) for v in param_value)
375
- else:
376
- # For complex types, try to create a simpler representation
377
- try:
378
- # Try to create a simple string representation
379
- formatted_parts = []
380
- for item in param_value:
381
- if isinstance(item, dict):
382
- # For objects, serialize key-value pairs
383
- item_parts = []
384
- for k, v in item.items():
385
- item_parts.append(f"{k}:{v}")
386
- formatted_parts.append(".".join(item_parts))
387
- else:
388
- formatted_parts.append(str(item))
389
-
390
- query_params[p.name] = ",".join(formatted_parts)
391
- except Exception as e:
392
- logger.warning(
393
- f"Failed to format complex array query parameter '{p.name}': {e}"
394
- )
395
- # Fallback to string representation
396
- query_params[p.name] = param_value
333
+ # Format array as comma-separated string when explode=False
334
+ formatted_value = format_array_parameter(
335
+ param_value, p.name, is_query_parameter=True
336
+ )
337
+ query_params[p.name] = formatted_value
397
338
  else:
398
339
  # Non-array parameters are passed as is
399
340
  query_params[p.name] = param_value
@@ -451,10 +392,11 @@ class OpenAPITool(Tool):
451
392
  # Try to parse as JSON first
452
393
  try:
453
394
  result = response.json()
454
- except (json.JSONDecodeError, ValueError):
455
- # Return text content if not JSON
456
- result = response.text
457
- return _convert_to_content(result)
395
+ if not isinstance(result, dict):
396
+ result = {"result": result}
397
+ return ToolResult(structured_content=result)
398
+ except json.JSONDecodeError:
399
+ return ToolResult(content=response.text)
458
400
 
459
401
  except httpx.HTTPStatusError as e:
460
402
  # Handle HTTP errors (4xx, 5xx)
fastmcp/server/proxy.py CHANGED
@@ -22,10 +22,9 @@ from fastmcp.resources import Resource, ResourceTemplate
22
22
  from fastmcp.resources.resource_manager import ResourceManager
23
23
  from fastmcp.server.context import Context
24
24
  from fastmcp.server.server import FastMCP
25
- from fastmcp.tools.tool import Tool
25
+ from fastmcp.tools.tool import Tool, ToolResult
26
26
  from fastmcp.tools.tool_manager import ToolManager
27
27
  from fastmcp.utilities.logging import get_logger
28
- from fastmcp.utilities.types import MCPContent
29
28
 
30
29
  if TYPE_CHECKING:
31
30
  from fastmcp.server import Context
@@ -67,7 +66,7 @@ class ProxyToolManager(ToolManager):
67
66
  tools_dict = await self.get_tools()
68
67
  return list(tools_dict.values())
69
68
 
70
- async def call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
69
+ async def call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
71
70
  """Calls a tool, trying local/mounted first, then proxy if not found."""
72
71
  try:
73
72
  # First try local and mounted tools
@@ -75,7 +74,11 @@ class ProxyToolManager(ToolManager):
75
74
  except NotFoundError:
76
75
  # If not found locally, try proxy
77
76
  async with self.client:
78
- return await self.client.call_tool(key, arguments)
77
+ result = await self.client.call_tool(key, arguments)
78
+ return ToolResult(
79
+ content=result.content,
80
+ structured_content=result.structured_content,
81
+ )
79
82
 
80
83
 
81
84
  class ProxyResourceManager(ResourceManager):
@@ -224,13 +227,14 @@ class ProxyTool(Tool):
224
227
  description=mcp_tool.description,
225
228
  parameters=mcp_tool.inputSchema,
226
229
  annotations=mcp_tool.annotations,
230
+ output_schema=mcp_tool.outputSchema,
227
231
  )
228
232
 
229
233
  async def run(
230
234
  self,
231
235
  arguments: dict[str, Any],
232
236
  context: Context | None = None,
233
- ) -> list[MCPContent]:
237
+ ) -> ToolResult:
234
238
  """Executes the tool by making a call through the client."""
235
239
  # This is where the remote execution logic lives.
236
240
  async with self._client:
@@ -240,7 +244,10 @@ class ProxyTool(Tool):
240
244
  )
241
245
  if result.isError:
242
246
  raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
243
- return result.content
247
+ return ToolResult(
248
+ content=result.content,
249
+ structured_content=result.structuredContent,
250
+ )
244
251
 
245
252
 
246
253
  class ProxyResource(Resource):