fastmcp 2.11.2__py3-none-any.whl → 2.11.3__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/client/logging.py CHANGED
@@ -13,7 +13,31 @@ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
13
13
 
14
14
 
15
15
  async def default_log_handler(message: LogMessage) -> None:
16
- logger.debug(f"Log received: {message}")
16
+ """Default handler that properly routes server log messages to appropriate log levels."""
17
+ msg = message.data.get("msg", str(message))
18
+ extra = message.data.get("extra", {})
19
+
20
+ # Map MCP log levels to Python logging levels
21
+ level_map = {
22
+ "debug": logger.debug,
23
+ "info": logger.info,
24
+ "notice": logger.info, # Python doesn't have 'notice', map to info
25
+ "warning": logger.warning,
26
+ "error": logger.error,
27
+ "critical": logger.critical,
28
+ "alert": logger.critical, # Map alert to critical
29
+ "emergency": logger.critical, # Map emergency to critical
30
+ }
31
+
32
+ # Get the appropriate logging function based on the message level
33
+ log_fn = level_map.get(message.level.lower(), logger.info)
34
+
35
+ # Include logger name if available
36
+ if message.logger:
37
+ msg = f"[{message.logger}] {msg}"
38
+
39
+ # Log with appropriate level and extra data
40
+ log_fn(f"Server log: {msg}", extra=extra)
17
41
 
18
42
 
19
43
  def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
@@ -6,7 +6,7 @@ import os
6
6
  import shutil
7
7
  import sys
8
8
  import warnings
9
- from collections.abc import AsyncIterator, Callable
9
+ from collections.abc import AsyncIterator
10
10
  from pathlib import Path
11
11
  from typing import Any, Literal, TypeVar, cast, overload
12
12
 
@@ -22,6 +22,7 @@ from mcp.client.session import (
22
22
  SamplingFnT,
23
23
  )
24
24
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
25
+ from mcp.shared._httpx_utils import McpHttpClientFactory
25
26
  from mcp.shared.memory import create_client_server_memory_streams
26
27
  from pydantic import AnyUrl
27
28
  from typing_extensions import TypedDict, Unpack
@@ -161,7 +162,7 @@ class SSETransport(ClientTransport):
161
162
  headers: dict[str, str] | None = None,
162
163
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
163
164
  sse_read_timeout: datetime.timedelta | float | int | None = None,
164
- httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
165
+ httpx_client_factory: McpHttpClientFactory | None = None,
165
166
  ):
166
167
  if isinstance(url, AnyUrl):
167
168
  url = str(url)
@@ -233,7 +234,7 @@ class StreamableHttpTransport(ClientTransport):
233
234
  headers: dict[str, str] | None = None,
234
235
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
235
236
  sse_read_timeout: datetime.timedelta | float | int | None = None,
236
- httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
237
+ httpx_client_factory: McpHttpClientFactory | None = None,
237
238
  ):
238
239
  if isinstance(url, AnyUrl):
239
240
  url = str(url)
@@ -113,7 +113,7 @@ def _determine_route_type(
113
113
  # We know mcp_type is not None here due to post_init validation
114
114
  assert route_map.mcp_type is not None
115
115
  logger.debug(
116
- f"Route {route.method} {route.path} matched mapping to {route_map.mcp_type.name}"
116
+ f"Route {route.method} {route.path} mapped to {route_map.mcp_type.name}"
117
117
  )
118
118
  return route_map
119
119
 
@@ -11,7 +11,7 @@ from jsonschema_path import SchemaPath
11
11
  from fastmcp.experimental.utilities.openapi import (
12
12
  HTTPRoute,
13
13
  extract_output_schema_from_responses,
14
- format_description_with_responses,
14
+ format_simple_description,
15
15
  parse_openapi_to_http_routes,
16
16
  )
17
17
  from fastmcp.experimental.utilities.openapi.director import RequestDirector
@@ -151,9 +151,6 @@ class FastMCPOpenAPI(FastMCP):
151
151
  try:
152
152
  self._spec = SchemaPath.from_dict(openapi_spec) # type: ignore[arg-type]
153
153
  self._director = RequestDirector(self._spec)
154
- logger.debug(
155
- "Initialized OpenAPI RequestDirector for stateless request building"
156
- )
157
154
  except Exception as e:
158
155
  logger.error(f"Failed to initialize RequestDirector: {e}")
159
156
  raise ValueError(f"Invalid OpenAPI specification: {e}") from e
@@ -270,7 +267,9 @@ class FastMCPOpenAPI(FastMCP):
270
267
 
271
268
  # Extract output schema from OpenAPI responses
272
269
  output_schema = extract_output_schema_from_responses(
273
- route.responses, route.schema_definitions, route.openapi_version
270
+ route.responses,
271
+ route.response_schemas,
272
+ route.openapi_version,
274
273
  )
275
274
 
276
275
  # Get a unique tool name
@@ -282,10 +281,9 @@ class FastMCPOpenAPI(FastMCP):
282
281
  or f"Executes {route.method} {route.path}"
283
282
  )
284
283
 
285
- # Format enhanced description with parameters and request body
286
- enhanced_description = format_description_with_responses(
284
+ # Use simplified description formatter for tools
285
+ enhanced_description = format_simple_description(
287
286
  base_description=base_description,
288
- responses=route.responses,
289
287
  parameters=route.parameters,
290
288
  request_body=route.request_body,
291
289
  )
@@ -318,9 +316,6 @@ class FastMCPOpenAPI(FastMCP):
318
316
 
319
317
  # Register the tool by directly assigning to the tools dictionary
320
318
  self._tool_manager._tools[final_tool_name] = tool
321
- logger.debug(
322
- f"Registered TOOL: {final_tool_name} ({route.method} {route.path}) with tags: {route.tags}"
323
- )
324
319
 
325
320
  def _create_openapi_resource(
326
321
  self,
@@ -337,10 +332,9 @@ class FastMCPOpenAPI(FastMCP):
337
332
  route.description or route.summary or f"Represents {route.path}"
338
333
  )
339
334
 
340
- # Format enhanced description with parameters and request body
341
- enhanced_description = format_description_with_responses(
335
+ # Use simplified description for resources
336
+ enhanced_description = format_simple_description(
342
337
  base_description=base_description,
343
- responses=route.responses,
344
338
  parameters=route.parameters,
345
339
  request_body=route.request_body,
346
340
  )
@@ -372,9 +366,6 @@ class FastMCPOpenAPI(FastMCP):
372
366
 
373
367
  # Register the resource by directly assigning to the resources dictionary
374
368
  self._resource_manager._resources[final_resource_uri] = resource
375
- logger.debug(
376
- f"Registered RESOURCE: {final_resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
377
- )
378
369
 
379
370
  def _create_openapi_template(
380
371
  self,
@@ -397,10 +388,9 @@ class FastMCPOpenAPI(FastMCP):
397
388
  route.description or route.summary or f"Template for {route.path}"
398
389
  )
399
390
 
400
- # Format enhanced description with parameters and request body
401
- enhanced_description = format_description_with_responses(
391
+ # Use simplified description for resource templates
392
+ enhanced_description = format_simple_description(
402
393
  base_description=base_description,
403
- responses=route.responses,
404
394
  parameters=route.parameters,
405
395
  request_body=route.request_body,
406
396
  )
@@ -455,9 +445,6 @@ class FastMCPOpenAPI(FastMCP):
455
445
 
456
446
  # Register the template by directly assigning to the templates dictionary
457
447
  self._resource_manager._templates[final_template_uri] = template
458
- logger.debug(
459
- f"Registered TEMPLATE: {final_template_uri} ({route.method} {route.path}) with tags: {route.tags}"
460
- )
461
448
 
462
449
 
463
450
  # Export public symbols
@@ -20,6 +20,7 @@ from .formatters import (
20
20
  format_deep_object_parameter,
21
21
  format_description_with_responses,
22
22
  format_json_for_description,
23
+ format_simple_description,
23
24
  generate_example_from_schema,
24
25
  )
25
26
 
@@ -28,7 +29,6 @@ from .schemas import (
28
29
  _combine_schemas,
29
30
  extract_output_schema_from_responses,
30
31
  clean_schema_for_display,
31
- _replace_ref_with_defs,
32
32
  _make_optional_parameter_nullable,
33
33
  )
34
34
 
@@ -55,12 +55,12 @@ __all__ = [
55
55
  "format_deep_object_parameter",
56
56
  "format_description_with_responses",
57
57
  "format_json_for_description",
58
+ "format_simple_description",
58
59
  "generate_example_from_schema",
59
60
  # Schemas
60
61
  "_combine_schemas",
61
62
  "extract_output_schema_from_responses",
62
63
  "clean_schema_for_display",
63
- "_replace_ref_with_defs",
64
64
  "_make_optional_parameter_nullable",
65
65
  # JSON Schema Converter
66
66
  "convert_openapi_schema_to_json_schema",
@@ -189,6 +189,39 @@ def format_json_for_description(data: Any, indent: int = 2) -> str:
189
189
  return f"```\nCould not serialize to JSON: {data}\n```"
190
190
 
191
191
 
192
+ def format_simple_description(
193
+ base_description: str,
194
+ parameters: list[ParameterInfo] | None = None,
195
+ request_body: RequestBodyInfo | None = None,
196
+ ) -> str:
197
+ """
198
+ Formats a simple description for MCP objects (tools, resources, prompts).
199
+ Excludes response details, examples, and verbose status codes.
200
+
201
+ Args:
202
+ base_description (str): The initial description to be formatted.
203
+ parameters (list[ParameterInfo] | None, optional): A list of parameter information.
204
+ request_body (RequestBodyInfo | None, optional): Information about the request body.
205
+
206
+ Returns:
207
+ str: The formatted description string with minimal details.
208
+ """
209
+ desc_parts = [base_description]
210
+
211
+ # Only add critical parameter information if they have descriptions
212
+ if parameters:
213
+ path_params = [p for p in parameters if p.location == "path" and p.description]
214
+ if path_params:
215
+ desc_parts.append("\n\n**Path Parameters:**")
216
+ for param in path_params:
217
+ desc_parts.append(f"\n- **{param.name}**: {param.description}")
218
+
219
+ # Skip query parameters, request body details, and all response information
220
+ # These are already captured in the inputSchema
221
+
222
+ return "\n".join(desc_parts)
223
+
224
+
192
225
  def format_description_with_responses(
193
226
  base_description: str,
194
227
  responses: dict[
@@ -351,5 +384,6 @@ __all__ = [
351
384
  "format_deep_object_parameter",
352
385
  "format_description_with_responses",
353
386
  "format_json_for_description",
387
+ "format_simple_description",
354
388
  "generate_example_from_schema",
355
389
  ]
@@ -58,9 +58,12 @@ class HTTPRoute(FastMCPBaseModel):
58
58
  responses: dict[str, ResponseInfo] = Field(
59
59
  default_factory=dict
60
60
  ) # Key: status code str
61
- schema_definitions: dict[str, JsonSchema] = Field(
61
+ request_schemas: dict[str, JsonSchema] = Field(
62
62
  default_factory=dict
63
- ) # Store component schemas
63
+ ) # Store schemas needed for input (parameters/request body)
64
+ response_schemas: dict[str, JsonSchema] = Field(
65
+ default_factory=dict
66
+ ) # Store schemas needed for output (responses)
64
67
  extensions: dict[str, Any] = Field(default_factory=dict)
65
68
  openapi_version: str | None = None
66
69
 
@@ -34,7 +34,10 @@ from .models import (
34
34
  RequestBodyInfo,
35
35
  ResponseInfo,
36
36
  )
37
- from .schemas import _combine_schemas_and_map_params, _replace_ref_with_defs
37
+ from .schemas import (
38
+ _combine_schemas_and_map_params,
39
+ _replace_ref_with_defs,
40
+ )
38
41
 
39
42
  logger = get_logger(__name__)
40
43
 
@@ -63,7 +66,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
63
66
  if openapi_version.startswith("3.0"):
64
67
  # Use OpenAPI 3.0 models
65
68
  openapi_30 = OpenAPI_30.model_validate(openapi_dict)
66
- logger.info(
69
+ logger.debug(
67
70
  f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
68
71
  )
69
72
  parser = OpenAPIParser(
@@ -81,7 +84,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
81
84
  else:
82
85
  # Default to OpenAPI 3.1 models
83
86
  openapi_31 = OpenAPI.model_validate(openapi_dict)
84
- logger.info(
87
+ logger.debug(
85
88
  f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
86
89
  )
87
90
  parser = OpenAPIParser(
@@ -207,7 +210,7 @@ class OpenAPIParser(
207
210
  try:
208
211
  resolved_schema = self._resolve_ref(schema_obj)
209
212
 
210
- if isinstance(resolved_schema, (self.schema_cls)):
213
+ if isinstance(resolved_schema, self.schema_cls):
211
214
  # Convert schema to dictionary
212
215
  result = resolved_schema.model_dump(
213
216
  mode="json", by_alias=True, exclude_none=True
@@ -220,7 +223,10 @@ class OpenAPIParser(
220
223
  )
221
224
  result = {}
222
225
 
223
- return _replace_ref_with_defs(result)
226
+ # Convert refs from OpenAPI format to JSON Schema format using recursive approach
227
+
228
+ result = _replace_ref_with_defs(result)
229
+ return result
224
230
  except ValueError as e:
225
231
  # Re-raise ValueError for external reference errors and other validation issues
226
232
  if "External or non-local reference not supported" in str(e):
@@ -396,80 +402,235 @@ class OpenAPIParser(
396
402
  )
397
403
  return None
398
404
 
405
+ def _is_success_status_code(self, status_code: str) -> bool:
406
+ """Check if a status code represents a successful response (2xx)."""
407
+ try:
408
+ code_int = int(status_code)
409
+ return 200 <= code_int < 300
410
+ except (ValueError, TypeError):
411
+ # Handle special cases like 'default' or other non-numeric codes
412
+ return status_code.lower() in ["default", "2xx"]
413
+
414
+ def _get_primary_success_response(
415
+ self, operation_responses: dict[str, Any]
416
+ ) -> tuple[str, Any] | None:
417
+ """Get the primary success response for an MCP tool. We only need one success response."""
418
+ if not operation_responses:
419
+ return None
420
+
421
+ # Priority order: 200, 201, 202, 204, 207, then any other 2xx
422
+ priority_codes = ["200", "201", "202", "204", "207"]
423
+
424
+ # First check priority codes
425
+ for code in priority_codes:
426
+ if code in operation_responses:
427
+ return (code, operation_responses[code])
428
+
429
+ # Then check any other 2xx codes
430
+ for status_code, resp_or_ref in operation_responses.items():
431
+ if self._is_success_status_code(status_code):
432
+ return (status_code, resp_or_ref)
433
+
434
+ # If no success codes found, return None (tool will have no output schema)
435
+ return None
436
+
399
437
  def _extract_responses(
400
438
  self, operation_responses: dict[str, Any] | None
401
439
  ) -> dict[str, ResponseInfo]:
402
- """Extract and resolve response information."""
440
+ """Extract and resolve response information. Only includes the primary success response for MCP tools."""
403
441
  extracted_responses: dict[str, ResponseInfo] = {}
404
442
 
405
443
  if not operation_responses:
406
444
  return extracted_responses
407
445
 
408
- for status_code, resp_or_ref in operation_responses.items():
409
- try:
410
- response = self._resolve_ref(resp_or_ref)
446
+ # For MCP tools, we only need the primary success response
447
+ primary_response = self._get_primary_success_response(operation_responses)
448
+ if not primary_response:
449
+ logger.debug("No success responses found, tool will have no output schema")
450
+ return extracted_responses
411
451
 
412
- if not isinstance(response, self.response_cls):
413
- logger.warning(
414
- f"Expected Response after resolving for status code {status_code}, "
415
- f"got {type(response)}. Skipping."
416
- )
417
- continue
452
+ status_code, resp_or_ref = primary_response
453
+ logger.debug(f"Using primary success response: {status_code}")
418
454
 
419
- # Create response info
420
- resp_info = ResponseInfo(description=response.description)
455
+ try:
456
+ response = self._resolve_ref(resp_or_ref)
421
457
 
422
- # Extract content schemas
423
- if hasattr(response, "content") and response.content:
424
- for media_type_str, media_type_obj in response.content.items():
425
- if (
426
- media_type_obj
427
- and hasattr(media_type_obj, "media_type_schema")
428
- and media_type_obj.media_type_schema
429
- ):
430
- try:
431
- schema_dict = self._extract_schema_as_dict(
432
- media_type_obj.media_type_schema
433
- )
434
- resp_info.content_schema[media_type_str] = schema_dict
435
- except ValueError as e:
436
- # Re-raise ValueError for external reference errors
437
- if (
438
- "External or non-local reference not supported"
439
- in str(e)
440
- ):
441
- raise
442
- logger.error(
443
- f"Failed to extract schema for media type '{media_type_str}' "
444
- f"in response {status_code}: {e}"
445
- )
446
- except Exception as e:
447
- logger.error(
448
- f"Failed to extract schema for media type '{media_type_str}' "
449
- f"in response {status_code}: {e}"
450
- )
451
-
452
- extracted_responses[str(status_code)] = resp_info
453
- except ValueError as e:
454
- # Re-raise ValueError for external reference errors
455
- if "External or non-local reference not supported" in str(e):
456
- raise
457
- ref_name = getattr(resp_or_ref, "ref", "unknown")
458
- logger.error(
459
- f"Failed to extract response for status code {status_code} "
460
- f"from reference '{ref_name}': {e}",
461
- exc_info=False,
462
- )
463
- except Exception as e:
464
- ref_name = getattr(resp_or_ref, "ref", "unknown")
465
- logger.error(
466
- f"Failed to extract response for status code {status_code} "
467
- f"from reference '{ref_name}': {e}",
468
- exc_info=False,
458
+ if not isinstance(response, self.response_cls):
459
+ logger.warning(
460
+ f"Expected Response after resolving for status code {status_code}, "
461
+ f"got {type(response)}. Returning empty responses."
469
462
  )
463
+ return extracted_responses
464
+
465
+ # Create response info
466
+ resp_info = ResponseInfo(description=response.description)
467
+
468
+ # Extract content schemas
469
+ if hasattr(response, "content") and response.content:
470
+ for media_type_str, media_type_obj in response.content.items():
471
+ if (
472
+ media_type_obj
473
+ and hasattr(media_type_obj, "media_type_schema")
474
+ and media_type_obj.media_type_schema
475
+ ):
476
+ try:
477
+ schema_dict = self._extract_schema_as_dict(
478
+ media_type_obj.media_type_schema
479
+ )
480
+ resp_info.content_schema[media_type_str] = schema_dict
481
+ except ValueError as e:
482
+ # Re-raise ValueError for external reference errors
483
+ if "External or non-local reference not supported" in str(
484
+ e
485
+ ):
486
+ raise
487
+ logger.error(
488
+ f"Failed to extract schema for media type '{media_type_str}' "
489
+ f"in response {status_code}: {e}"
490
+ )
491
+ except Exception as e:
492
+ logger.error(
493
+ f"Failed to extract schema for media type '{media_type_str}' "
494
+ f"in response {status_code}: {e}"
495
+ )
496
+
497
+ extracted_responses[str(status_code)] = resp_info
498
+ except ValueError as e:
499
+ # Re-raise ValueError for external reference errors
500
+ if "External or non-local reference not supported" in str(e):
501
+ raise
502
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
503
+ logger.error(
504
+ f"Failed to extract response for status code {status_code} "
505
+ f"from reference '{ref_name}': {e}",
506
+ exc_info=False,
507
+ )
508
+ except Exception as e:
509
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
510
+ logger.error(
511
+ f"Failed to extract response for status code {status_code} "
512
+ f"from reference '{ref_name}': {e}",
513
+ exc_info=False,
514
+ )
470
515
 
471
516
  return extracted_responses
472
517
 
518
+ def _extract_schema_dependencies(
519
+ self,
520
+ schema: dict,
521
+ all_schemas: dict[str, Any],
522
+ collected: set[str] | None = None,
523
+ ) -> set[str]:
524
+ """
525
+ Extract all schema names referenced by a schema (including transitive dependencies).
526
+
527
+ Args:
528
+ schema: The schema to analyze
529
+ all_schemas: All available schema definitions
530
+ collected: Set of already collected schema names (for recursion)
531
+
532
+ Returns:
533
+ Set of schema names that are referenced
534
+ """
535
+ if collected is None:
536
+ collected = set()
537
+
538
+ def find_refs(obj):
539
+ """Recursively find all $ref references."""
540
+ if isinstance(obj, dict):
541
+ if "$ref" in obj and isinstance(obj["$ref"], str):
542
+ ref = obj["$ref"]
543
+ # Handle both converted and unconverted refs
544
+ if ref.startswith("#/$defs/"):
545
+ schema_name = ref.split("/")[-1]
546
+ elif ref.startswith("#/components/schemas/"):
547
+ schema_name = ref.split("/")[-1]
548
+ else:
549
+ return
550
+
551
+ # Add this schema and recursively find its dependencies
552
+ if schema_name not in collected and schema_name in all_schemas:
553
+ collected.add(schema_name)
554
+ # Recursively find dependencies of this schema
555
+ find_refs(all_schemas[schema_name])
556
+
557
+ # Continue searching in all values
558
+ for value in obj.values():
559
+ find_refs(value)
560
+ elif isinstance(obj, list):
561
+ for item in obj:
562
+ find_refs(item)
563
+
564
+ find_refs(schema)
565
+ return collected
566
+
567
+ def _extract_input_schema_dependencies(
568
+ self,
569
+ parameters: list[ParameterInfo],
570
+ request_body: RequestBodyInfo | None,
571
+ all_schemas: dict[str, Any],
572
+ ) -> dict[str, Any]:
573
+ """
574
+ Extract only the schema definitions needed for input (parameters and request body).
575
+
576
+ Args:
577
+ parameters: Route parameters
578
+ request_body: Route request body
579
+ all_schemas: All available schema definitions
580
+
581
+ Returns:
582
+ Dictionary containing only the schemas needed for input
583
+ """
584
+ needed_schemas = set()
585
+
586
+ # Check parameters for schema references
587
+ for param in parameters:
588
+ if param.schema_:
589
+ deps = self._extract_schema_dependencies(param.schema_, all_schemas)
590
+ needed_schemas.update(deps)
591
+
592
+ # Check request body for schema references
593
+ if request_body and request_body.content_schema:
594
+ for content_schema in request_body.content_schema.values():
595
+ deps = self._extract_schema_dependencies(content_schema, all_schemas)
596
+ needed_schemas.update(deps)
597
+
598
+ # Return only the needed input schemas
599
+ return {
600
+ name: all_schemas[name] for name in needed_schemas if name in all_schemas
601
+ }
602
+
603
+ def _extract_output_schema_dependencies(
604
+ self,
605
+ responses: dict[str, ResponseInfo],
606
+ all_schemas: dict[str, Any],
607
+ ) -> dict[str, Any]:
608
+ """
609
+ Extract only the schema definitions needed for outputs (responses).
610
+
611
+ Args:
612
+ responses: Route responses
613
+ all_schemas: All available schema definitions
614
+
615
+ Returns:
616
+ Dictionary containing only the schemas needed for outputs
617
+ """
618
+ needed_schemas = set()
619
+
620
+ # Check responses for schema references
621
+ for response in responses.values():
622
+ if response.content_schema:
623
+ for content_schema in response.content_schema.values():
624
+ deps = self._extract_schema_dependencies(
625
+ content_schema, all_schemas
626
+ )
627
+ needed_schemas.update(deps)
628
+
629
+ # Return only the needed output schemas
630
+ return {
631
+ name: all_schemas[name] for name in needed_schemas if name in all_schemas
632
+ }
633
+
473
634
  def parse(self) -> list[HTTPRoute]:
474
635
  """Parse the OpenAPI schema into HTTP routes."""
475
636
  routes: list[HTTPRoute] = []
@@ -499,6 +660,13 @@ class OpenAPIParser(
499
660
  f"Failed to extract schema definition '{name}': {e}"
500
661
  )
501
662
 
663
+ # Convert schema definitions refs from OpenAPI to JSON Schema format (once)
664
+ if schema_definitions:
665
+ # Convert each schema definition recursively
666
+ for name, schema in schema_definitions.items():
667
+ if isinstance(schema, dict):
668
+ schema_definitions[name] = _replace_ref_with_defs(schema)
669
+
502
670
  # Process paths and operations
503
671
  for path_str, path_item_obj in self.openapi.paths.items():
504
672
  if not isinstance(path_item_obj, self.path_item_cls):
@@ -552,6 +720,17 @@ class OpenAPIParser(
552
720
  if k.startswith("x-")
553
721
  }
554
722
 
723
+ # Extract schemas separately for input and output
724
+ input_schemas = self._extract_input_schema_dependencies(
725
+ parameters,
726
+ request_body_info,
727
+ schema_definitions,
728
+ )
729
+ output_schemas = self._extract_output_schema_dependencies(
730
+ responses,
731
+ schema_definitions,
732
+ )
733
+
555
734
  # Create initial route without pre-calculated fields
556
735
  route = HTTPRoute(
557
736
  path=path_str,
@@ -563,7 +742,8 @@ class OpenAPIParser(
563
742
  parameters=parameters,
564
743
  request_body=request_body_info,
565
744
  responses=responses,
566
- schema_definitions=schema_definitions,
745
+ request_schemas=input_schemas,
746
+ response_schemas=output_schemas,
567
747
  extensions=extensions,
568
748
  openapi_version=self.openapi_version,
569
749
  )
@@ -571,7 +751,8 @@ class OpenAPIParser(
571
751
  # Pre-calculate schema and parameter mapping for performance
572
752
  try:
573
753
  flat_schema, param_map = _combine_schemas_and_map_params(
574
- route
754
+ route,
755
+ convert_refs=False, # Parser already converted refs
575
756
  )
576
757
  route.flat_param_schema = flat_schema
577
758
  route.parameter_map = param_map
@@ -586,9 +767,6 @@ class OpenAPIParser(
586
767
  }
587
768
  route.parameter_map = {}
588
769
  routes.append(route)
589
- logger.info(
590
- f"Successfully extracted route: {method_upper} {path_str}"
591
- )
592
770
  except ValueError as op_error:
593
771
  # Re-raise ValueError for external reference errors
594
772
  if "External or non-local reference not supported" in str(
@@ -607,7 +785,7 @@ class OpenAPIParser(
607
785
  exc_info=True,
608
786
  )
609
787
 
610
- logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
788
+ logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
611
789
  return routes
612
790
 
613
791