openapi-to-mcp-cli 0.9.2__tar.gz → 0.9.3__tar.gz
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.
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/PKG-INFO +7 -3
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/README.md +6 -2
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/generator.py +1 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/generation_service.py +1 -0
- openapi_to_mcp_cli-0.9.3/openapi_to_mcp/common/spec_compatibility.py +152 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/doctor/analyzer.py +9 -6
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/doctor/security.py +6 -4
- openapi_to_mcp_cli-0.9.3/openapi_to_mcp/doctor/swagger.py +38 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/mapper.py +65 -16
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/output_schema.py +9 -2
- openapi_to_mcp_cli-0.9.3/openapi_to_mcp/policy/swagger.py +64 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/converter.py +4 -2
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/array_schema.py +2 -3
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/base.py +9 -1
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/common.py +2 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/composition.py +2 -2
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/number_schema.py +2 -3
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/object_schema.py +2 -3
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/reference.py +2 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/string_schema.py +2 -2
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/README.md.j2 +9 -0
- openapi_to_mcp_cli-0.9.3/openapi_to_mcp/templates/src/runtime/query.ts.j2 +106 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/request.ts.j2 +15 -1
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/serialization.ts.j2 +30 -4
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/pyproject.toml +1 -1
- openapi_to_mcp_cli-0.9.3/scripts/assert_container_server.py +43 -0
- openapi_to_mcp_cli-0.9.3/scripts/e2e_container.sh +39 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_policy_changes.py +3 -1
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/workflow.sh +2 -2
- openapi_to_mcp_cli-0.9.3/tests/integration/query_serialization_spec.py +81 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli.py +120 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_doctor.py +33 -0
- openapi_to_mcp_cli-0.9.3/tests/integration/test_cli_policy_errors.py +106 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_runtime_validation.py +41 -0
- openapi_to_mcp_cli-0.9.3/tests/integration/test_query_serialization.py +131 -0
- openapi_to_mcp_cli-0.9.3/tests/integration/test_swagger_policy_compatibility.py +199 -0
- openapi_to_mcp_cli-0.9.3/tests/unit/test_doctor_analyzer.py +259 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_generator_runtime_files.py +2 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper.py +98 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper_output_schema.py +32 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper_request_body_contracts.py +31 -0
- openapi_to_mcp_cli-0.9.3/tests/unit/test_mapper_tool_naming.py +63 -0
- openapi_to_mcp_cli-0.9.3/tests/unit/test_release_container_reference.py +30 -0
- openapi_to_mcp_cli-0.9.3/tests/unit/test_release_preflight.py +36 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_schema_converter.py +67 -0
- openapi_to_mcp_cli-0.9.3/tests/unit/test_swagger_compatibility.py +115 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/uv.lock +1 -1
- openapi_to_mcp_cli-0.9.2/tests/unit/test_doctor_analyzer.py +0 -112
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/.gitignore +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/LICENSE +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/spec_loader.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/http_redirects.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/models.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/response_formatting.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/server_tester.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/stdio_transport.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/adapters/testing/streamable_http_transport.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/cli.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/diff.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/doctor.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/generate.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/generation_context.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/generation_models.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/options.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/policy_support.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/run.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/run_support.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/runtime_overrides.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/test_server.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/decorators.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/error_policy.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/exceptions.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/logger.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/performance_presets.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/terminal.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/tool_runtime.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/common/utils.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/diff/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/diff/analyzer.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/diff/models.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/diff/surface.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/doctor/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/doctor/models.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/tool_description.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/tool_examples.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/tool_grouping.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/utils.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/applier.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/loader.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/models.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/parser.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/settings.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/policy/validators.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/.env.example.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/package.json.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/custom/tools.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/index.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/audit.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/auth.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/cache.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/circuit_breaker.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/config.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/errors.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/executor.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/executor_support.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/generated.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/http_transport.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/limiter.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/observability.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/performance_preset.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/rate_limit.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/redaction.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/request_context.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/resilience.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/response.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/retry.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/tool_access.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/runtime/validation.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/server.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/transport_stdio.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/src/transport_streamable_http.ts.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/templates/tsconfig.json.j2 +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_access.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_audit.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_cached_audit.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_common.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_performance.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_resilience.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_generated_server_suites.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/assert_runtime_observability.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/detect_release.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/e2e_cli_matrix.sh +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/e2e_generated_server.sh +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/mock_target_api.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_automation.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_metadata.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_policy.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_policy_config.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/scripts/release_policy_lock.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/conftest.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/constants.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/cli_generation_specs.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_audit_controls.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_customization.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_diff.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_diff_behavior.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_generate_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_generation_outputs.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_observability.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_output_schema.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_performance_controls.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_policy_config.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_policy_execution.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_resilience_controls.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_tool_access.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_tool_grouping.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_tool_shaping.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/integration/test_cli_xquik_fixture.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/audit_openapi.yaml +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/auth_openapi.yaml +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/release_contracts/auth.yaml +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/release_contracts/basic.yaml +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/test_openapi.json +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/resources/test_openapi.yaml +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/__init__.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_assertion_script_requests.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_assertion_wire_metadata.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_cli_help.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_generate.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_access_env.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_audit_env.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_env.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_errors.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_performance_env.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_run_resilience_env.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_test_server.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_commands_test_server_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_common_logger.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_common_utils.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_decorators.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_diff_analyzer.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_diff_surface.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_generator.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_generator_transport.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper_output_schema_selection.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper_robustness_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapper_tool_shaping.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_mapping_utils.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_performance_presets.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_policy_applier.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_policy_loader.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_automation.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_detection.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_metadata.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_policy.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_release_workflow.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_response_formatting.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_schema_handlers_base.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_schema_handlers_composition.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_schema_handlers_number.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_server_tester.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_server_tester_stdio.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_server_tester_streamable_contracts.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_spec_loader.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_streamable_http_metadata.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_streamable_http_redirects.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_tool_description.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_tool_grouping.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/unit/test_tool_runtime.py +0 -0
- {openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openapi-to-mcp-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Generate, run, and test Node.js/TypeScript MCP servers from OpenAPI specifications.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nihal1294/openapi-to-mcp
|
|
6
6
|
Project-URL: Documentation, https://nihal1294.github.io/openapi-to-mcp/
|
|
@@ -75,8 +75,8 @@ Run directly from a spec:
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
openapi-to-mcp run \
|
|
78
|
-
--openapi-json https://
|
|
79
|
-
--target-api-base-url https://
|
|
78
|
+
--openapi-json https://petstore3.swagger.io/api/v3/openapi.json \
|
|
79
|
+
--target-api-base-url https://petstore3.swagger.io/api/v3
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Smoke-test a running server:
|
|
@@ -116,6 +116,10 @@ Public product documentation lives on GitHub Pages:
|
|
|
116
116
|
- CLI E2E coverage for `generate`, `run`, and `test-server`
|
|
117
117
|
- version-aware PyPI and GitHub Releases automation on `master`
|
|
118
118
|
|
|
119
|
+
## Swagger 2 support
|
|
120
|
+
|
|
121
|
+
Swagger 2 response-only operations and base URL fields are supported. Convert operations that use Swagger 2 body or form parameters, top-level parameter types, security definitions, or unsupported protocol overrides to OpenAPI 3 before generating. Run `openapi-to-mcp doctor` to identify the affected operation and field.
|
|
122
|
+
|
|
119
123
|
## Development
|
|
120
124
|
|
|
121
125
|
For repository-local workflows, use the repo docs directly:
|
|
@@ -45,8 +45,8 @@ Run directly from a spec:
|
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
openapi-to-mcp run \
|
|
48
|
-
--openapi-json https://
|
|
49
|
-
--target-api-base-url https://
|
|
48
|
+
--openapi-json https://petstore3.swagger.io/api/v3/openapi.json \
|
|
49
|
+
--target-api-base-url https://petstore3.swagger.io/api/v3
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Smoke-test a running server:
|
|
@@ -86,6 +86,10 @@ Public product documentation lives on GitHub Pages:
|
|
|
86
86
|
- CLI E2E coverage for `generate`, `run`, and `test-server`
|
|
87
87
|
- version-aware PyPI and GitHub Releases automation on `master`
|
|
88
88
|
|
|
89
|
+
## Swagger 2 support
|
|
90
|
+
|
|
91
|
+
Swagger 2 response-only operations and base URL fields are supported. Convert operations that use Swagger 2 body or form parameters, top-level parameter types, security definitions, or unsupported protocol overrides to OpenAPI 3 before generating. Run `openapi-to-mcp doctor` to identify the affected operation and field.
|
|
92
|
+
|
|
89
93
|
## Development
|
|
90
94
|
|
|
91
95
|
For repository-local workflows, use the repo docs directly:
|
{openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/commands/generation_service.py
RENAMED
|
@@ -65,6 +65,7 @@ def _map_tools(
|
|
|
65
65
|
strict=behavior.strict,
|
|
66
66
|
on_mapping_error=behavior.on_mapping_error,
|
|
67
67
|
on_schema_error=behavior.on_schema_error,
|
|
68
|
+
policy_config=request.policy_config,
|
|
68
69
|
)
|
|
69
70
|
tools = apply_policy(mapper.map_tools(), request.policy_config)
|
|
70
71
|
tools = apply_tool_grouping(tools, behavior.tool_grouping)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Compatibility checks shared by Swagger 2 diagnostics and mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SwaggerFinding:
|
|
11
|
+
"""One Swagger 2 construct that cannot be represented by the mapper."""
|
|
12
|
+
|
|
13
|
+
field: str
|
|
14
|
+
location: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def swagger_operation_findings(
|
|
18
|
+
spec: dict[str, Any],
|
|
19
|
+
method: str,
|
|
20
|
+
path: str,
|
|
21
|
+
path_item: dict[str, Any],
|
|
22
|
+
operation: dict[str, Any],
|
|
23
|
+
) -> list[SwaggerFinding]:
|
|
24
|
+
"""Return unsupported Swagger 2 constructs used by one operation."""
|
|
25
|
+
if spec.get("swagger") != "2.0":
|
|
26
|
+
return []
|
|
27
|
+
base = f"paths.{path}"
|
|
28
|
+
findings = _parameter_findings(
|
|
29
|
+
spec, path_item.get("parameters"), f"{base}.parameters"
|
|
30
|
+
)
|
|
31
|
+
findings.extend(
|
|
32
|
+
_parameter_findings(
|
|
33
|
+
spec, operation.get("parameters"), f"{base}.{method.lower()}.parameters"
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
findings.extend(_security_findings(spec, operation, base, method))
|
|
37
|
+
findings.extend(_scheme_findings(spec, operation, base, method))
|
|
38
|
+
return list(dict.fromkeys(findings))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_swagger_finding(finding: SwaggerFinding) -> str:
|
|
42
|
+
"""Format one compatibility finding as an actionable mapping error."""
|
|
43
|
+
return (
|
|
44
|
+
f"unsupported Swagger 2 construct `{finding.field}` at `{finding.location}`; "
|
|
45
|
+
"convert this operation to OpenAPI 3 before generation."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parameter_findings(
|
|
50
|
+
spec: dict[str, Any], parameters: object, location: str
|
|
51
|
+
) -> list[SwaggerFinding]:
|
|
52
|
+
if not isinstance(parameters, list):
|
|
53
|
+
return []
|
|
54
|
+
findings: list[SwaggerFinding] = []
|
|
55
|
+
for index, candidate in enumerate(parameters):
|
|
56
|
+
parameter = _resolve_parameter(spec, candidate)
|
|
57
|
+
field_location = f"{location}[{index}]"
|
|
58
|
+
if not isinstance(parameter, dict):
|
|
59
|
+
if isinstance(candidate, dict) and "$ref" in candidate:
|
|
60
|
+
findings.append(SwaggerFinding("$ref", f"{field_location}.$ref"))
|
|
61
|
+
continue
|
|
62
|
+
parameter_in = parameter.get("in")
|
|
63
|
+
if parameter_in in {"body", "formData"}:
|
|
64
|
+
findings.append(
|
|
65
|
+
SwaggerFinding(f"in: {parameter_in}", f"{field_location}.in")
|
|
66
|
+
)
|
|
67
|
+
if (
|
|
68
|
+
parameter_in in {"path", "query", "header", "cookie"}
|
|
69
|
+
and "type" in parameter
|
|
70
|
+
):
|
|
71
|
+
findings.append(SwaggerFinding("type", f"{field_location}.type"))
|
|
72
|
+
return findings
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_parameter(
|
|
76
|
+
spec: dict[str, Any], candidate: object
|
|
77
|
+
) -> dict[str, Any] | None:
|
|
78
|
+
if not isinstance(candidate, dict):
|
|
79
|
+
return None
|
|
80
|
+
current = candidate
|
|
81
|
+
for _ in range(20):
|
|
82
|
+
ref = current.get("$ref")
|
|
83
|
+
if ref is None:
|
|
84
|
+
return current
|
|
85
|
+
if not isinstance(ref, str):
|
|
86
|
+
return None
|
|
87
|
+
resolved = _resolve_local_ref(spec, ref)
|
|
88
|
+
if not isinstance(resolved, dict):
|
|
89
|
+
return None
|
|
90
|
+
current = resolved
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_local_ref(spec: dict[str, Any], ref: str) -> object:
|
|
95
|
+
if not ref.startswith("#/"):
|
|
96
|
+
return None
|
|
97
|
+
current: object = spec
|
|
98
|
+
for token in ref[2:].split("/"):
|
|
99
|
+
if not isinstance(current, dict):
|
|
100
|
+
return None
|
|
101
|
+
current = current.get(token.replace("~1", "/").replace("~0", "~"))
|
|
102
|
+
return current
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _security_findings(
|
|
106
|
+
spec: dict[str, Any], operation: dict[str, Any], base: str, method: str
|
|
107
|
+
) -> list[SwaggerFinding]:
|
|
108
|
+
location = (
|
|
109
|
+
f"{base}.{method.lower()}.security" if "security" in operation else "security"
|
|
110
|
+
)
|
|
111
|
+
requirements = (
|
|
112
|
+
operation.get("security") if "security" in operation else spec.get("security")
|
|
113
|
+
)
|
|
114
|
+
if not isinstance(requirements, list) or not requirements:
|
|
115
|
+
return []
|
|
116
|
+
definitions = spec.get("securityDefinitions")
|
|
117
|
+
names = {
|
|
118
|
+
name
|
|
119
|
+
for item in requirements
|
|
120
|
+
if isinstance(item, dict)
|
|
121
|
+
for name in item
|
|
122
|
+
if isinstance(name, str)
|
|
123
|
+
}
|
|
124
|
+
if not names:
|
|
125
|
+
return []
|
|
126
|
+
field = (
|
|
127
|
+
"securityDefinitions"
|
|
128
|
+
if isinstance(definitions, dict) and names & definitions.keys()
|
|
129
|
+
else "security"
|
|
130
|
+
)
|
|
131
|
+
return [SwaggerFinding(field, location)]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _scheme_findings(
|
|
135
|
+
spec: dict[str, Any], operation: dict[str, Any], base: str, method: str
|
|
136
|
+
) -> list[SwaggerFinding]:
|
|
137
|
+
selected_scheme = _document_scheme(spec)
|
|
138
|
+
if selected_scheme not in {"http", "https"}:
|
|
139
|
+
return [SwaggerFinding("schemes", "schemes[0]")]
|
|
140
|
+
if "schemes" not in operation:
|
|
141
|
+
return []
|
|
142
|
+
operation_schemes = operation.get("schemes")
|
|
143
|
+
if isinstance(operation_schemes, list) and selected_scheme in operation_schemes:
|
|
144
|
+
return []
|
|
145
|
+
return [SwaggerFinding("schemes", f"{base}.{method.lower()}.schemes")]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _document_scheme(spec: dict[str, Any]) -> str:
|
|
149
|
+
schemes = spec.get("schemes")
|
|
150
|
+
if isinstance(schemes, list) and schemes and isinstance(schemes[0], str):
|
|
151
|
+
return schemes[0]
|
|
152
|
+
return "https"
|
|
@@ -10,6 +10,7 @@ from openapi_to_mcp.doctor.security import (
|
|
|
10
10
|
referenced_scheme_names,
|
|
11
11
|
security_schemes,
|
|
12
12
|
)
|
|
13
|
+
from openapi_to_mcp.doctor.swagger import report_swagger_compatibility
|
|
13
14
|
from openapi_to_mcp.mapping.utils import generate_tool_name
|
|
14
15
|
|
|
15
16
|
_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head", "trace"}
|
|
@@ -39,6 +40,7 @@ class DoctorAnalyzer:
|
|
|
39
40
|
self._check_missing_operation_ids(report, operations)
|
|
40
41
|
self._check_generated_name_collisions(report, operations)
|
|
41
42
|
self._check_security(report, operations)
|
|
43
|
+
report_swagger_compatibility(report, self.spec, operations)
|
|
42
44
|
self._check_schema_unions(report, operations)
|
|
43
45
|
return report
|
|
44
46
|
|
|
@@ -123,12 +125,13 @@ class DoctorAnalyzer:
|
|
|
123
125
|
operations: list[tuple[str, str, dict[str, Any]]],
|
|
124
126
|
) -> None:
|
|
125
127
|
schemes = security_schemes(self.spec)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
if any("security" not in operation for _, _, operation in operations):
|
|
129
|
+
self._check_security_requirements(
|
|
130
|
+
report,
|
|
131
|
+
source=self.spec.get("security", []),
|
|
132
|
+
schemes=schemes,
|
|
133
|
+
location="security",
|
|
134
|
+
)
|
|
132
135
|
for method, path, operation in operations:
|
|
133
136
|
security = operation.get("security")
|
|
134
137
|
if security is None:
|
|
@@ -13,10 +13,12 @@ if TYPE_CHECKING:
|
|
|
13
13
|
def security_schemes(spec: dict[str, Any]) -> dict[str, Any]:
|
|
14
14
|
"""Return normalized OpenAPI security scheme definitions."""
|
|
15
15
|
components = spec.get("components", {})
|
|
16
|
-
if
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if isinstance(components, dict):
|
|
17
|
+
schemes = components.get("securitySchemes")
|
|
18
|
+
if isinstance(schemes, dict):
|
|
19
|
+
return schemes
|
|
20
|
+
definitions = spec.get("securityDefinitions", {})
|
|
21
|
+
return definitions if isinstance(definitions, dict) else {}
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def referenced_scheme_names(source: object) -> list[str]:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Swagger 2 compatibility diagnostics for doctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from openapi_to_mcp.common.spec_compatibility import swagger_operation_findings
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from openapi_to_mcp.doctor.models import DoctorReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def report_swagger_compatibility(
|
|
14
|
+
report: DoctorReport,
|
|
15
|
+
spec: dict[str, Any],
|
|
16
|
+
operations: list[tuple[str, str, dict[str, Any]]],
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Add one diagnostic for each unique unsupported Swagger 2 construct."""
|
|
19
|
+
paths = spec.get("paths", {})
|
|
20
|
+
if not isinstance(paths, dict):
|
|
21
|
+
return
|
|
22
|
+
seen = set()
|
|
23
|
+
for method, path, operation in operations:
|
|
24
|
+
path_item = paths.get(path)
|
|
25
|
+
if not isinstance(path_item, dict):
|
|
26
|
+
continue
|
|
27
|
+
for finding in swagger_operation_findings(
|
|
28
|
+
spec, method, path, path_item, operation
|
|
29
|
+
):
|
|
30
|
+
if finding in seen:
|
|
31
|
+
continue
|
|
32
|
+
seen.add(finding)
|
|
33
|
+
report.add_error(
|
|
34
|
+
"unsupported_swagger_construct",
|
|
35
|
+
f"Swagger 2 `{finding.field}` cannot be faithfully generated.",
|
|
36
|
+
finding.location,
|
|
37
|
+
"Convert this operation to OpenAPI 3 before generating an MCP server.",
|
|
38
|
+
)
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
"""Maps OpenAPI operations to MCP tool definitions."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import logging
|
|
4
|
-
from typing import Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
5
7
|
|
|
6
8
|
from openapi_to_mcp.common import MappingError, SchemaError
|
|
7
9
|
from openapi_to_mcp.common.error_policy import ErrorMode, resolve_error_mode
|
|
10
|
+
from openapi_to_mcp.common.exceptions import PolicyConfigError
|
|
11
|
+
from openapi_to_mcp.common.spec_compatibility import (
|
|
12
|
+
format_swagger_finding,
|
|
13
|
+
swagger_operation_findings,
|
|
14
|
+
)
|
|
8
15
|
from openapi_to_mcp.mapping.output_schema import extract_output_schema
|
|
9
16
|
from openapi_to_mcp.mapping.tool_description import build_tool_description
|
|
10
17
|
from openapi_to_mcp.mapping.tool_examples import (
|
|
@@ -13,12 +20,16 @@ from openapi_to_mcp.mapping.tool_examples import (
|
|
|
13
20
|
build_input_examples,
|
|
14
21
|
)
|
|
15
22
|
from openapi_to_mcp.mapping.utils import generate_tool_name
|
|
23
|
+
from openapi_to_mcp.policy.swagger import effective_swagger_findings
|
|
16
24
|
from openapi_to_mcp.schema.converter import (
|
|
17
25
|
SchemaConverter,
|
|
18
26
|
openapi_schema_to_json_schema,
|
|
19
27
|
)
|
|
20
28
|
from openapi_to_mcp.schema.handlers.reference import ReferenceHandler
|
|
21
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from openapi_to_mcp.policy.models import PolicyConfig
|
|
32
|
+
|
|
22
33
|
logger = logging.getLogger(__name__)
|
|
23
34
|
|
|
24
35
|
|
|
@@ -32,6 +43,7 @@ class Mapper:
|
|
|
32
43
|
strict: bool = True,
|
|
33
44
|
on_mapping_error: ErrorMode | None = None,
|
|
34
45
|
on_schema_error: ErrorMode | None = None,
|
|
46
|
+
policy_config: PolicyConfig | None = None,
|
|
35
47
|
) -> None:
|
|
36
48
|
"""
|
|
37
49
|
Initialize the mapper with the loaded OpenAPI spec.
|
|
@@ -48,6 +60,7 @@ class Mapper:
|
|
|
48
60
|
raise MappingError(err_msg)
|
|
49
61
|
self.spec = spec
|
|
50
62
|
self.strict = strict
|
|
63
|
+
self.policy_config = policy_config
|
|
51
64
|
self.on_mapping_error = resolve_error_mode(on_mapping_error, strict=strict)
|
|
52
65
|
self.on_schema_error = resolve_error_mode(on_schema_error, strict=strict)
|
|
53
66
|
self.mcp_tools: list[dict[str, Any]] = []
|
|
@@ -109,16 +122,42 @@ class Mapper:
|
|
|
109
122
|
operation,
|
|
110
123
|
merged_parameters,
|
|
111
124
|
)
|
|
125
|
+
self._raise_if_unsupported_swagger(
|
|
126
|
+
path_item, operation, tool_definition
|
|
127
|
+
)
|
|
128
|
+
self._register_tool_name(
|
|
129
|
+
operation.get("operationId")
|
|
130
|
+
or generate_tool_name(method, path),
|
|
131
|
+
tool_definition["name"],
|
|
132
|
+
)
|
|
112
133
|
self.mcp_tools.append(tool_definition)
|
|
113
134
|
except SchemaError as exc:
|
|
114
135
|
self._handle_schema_error(method, path, exc)
|
|
115
|
-
except
|
|
116
|
-
|
|
136
|
+
except PolicyConfigError:
|
|
137
|
+
raise
|
|
117
138
|
except Exception as exc: # noqa: BLE001
|
|
118
139
|
self._handle_mapping_error(method, path, exc)
|
|
119
140
|
|
|
120
141
|
return self.mcp_tools
|
|
121
142
|
|
|
143
|
+
def _raise_if_unsupported_swagger(
|
|
144
|
+
self,
|
|
145
|
+
path_item: dict[str, Any],
|
|
146
|
+
operation: dict[str, Any],
|
|
147
|
+
tool: dict[str, Any],
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Raise when this operation needs unsupported Swagger 2 behavior."""
|
|
150
|
+
findings = swagger_operation_findings(
|
|
151
|
+
self.spec,
|
|
152
|
+
tool["_original_method"],
|
|
153
|
+
tool["_original_path"],
|
|
154
|
+
path_item,
|
|
155
|
+
operation,
|
|
156
|
+
)
|
|
157
|
+
findings = effective_swagger_findings(findings, tool, self.policy_config)
|
|
158
|
+
if findings:
|
|
159
|
+
raise MappingError(format_swagger_finding(findings[0]))
|
|
160
|
+
|
|
122
161
|
def get_report(self) -> dict[str, Any]:
|
|
123
162
|
"""Get mapper warnings and skipped operations for generation report."""
|
|
124
163
|
return {
|
|
@@ -285,13 +324,9 @@ class Mapper:
|
|
|
285
324
|
|
|
286
325
|
return (name, location)
|
|
287
326
|
|
|
288
|
-
def
|
|
289
|
-
"""
|
|
327
|
+
def _resolve_unique_tool_name(self, candidate_name: str) -> str:
|
|
328
|
+
"""Return a unique tool name without reserving it."""
|
|
290
329
|
if candidate_name not in self._used_tool_names:
|
|
291
|
-
self._used_tool_names.add(candidate_name)
|
|
292
|
-
self._tool_name_counts[candidate_name] = max(
|
|
293
|
-
self._tool_name_counts.get(candidate_name, 0), 1
|
|
294
|
-
)
|
|
295
330
|
return candidate_name
|
|
296
331
|
|
|
297
332
|
if self.on_mapping_error == "fail":
|
|
@@ -306,12 +341,26 @@ class Mapper:
|
|
|
306
341
|
if deduped_name not in self._used_tool_names:
|
|
307
342
|
break
|
|
308
343
|
|
|
309
|
-
|
|
310
|
-
|
|
344
|
+
return deduped_name
|
|
345
|
+
|
|
346
|
+
def _register_tool_name(
|
|
347
|
+
self,
|
|
348
|
+
candidate_name: str,
|
|
349
|
+
tool_name: str,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Reserve a tool name after the operation has been accepted."""
|
|
352
|
+
self._used_tool_names.add(tool_name)
|
|
353
|
+
if tool_name == candidate_name:
|
|
354
|
+
self._tool_name_counts[candidate_name] = max(
|
|
355
|
+
self._tool_name_counts.get(candidate_name, 0), 1
|
|
356
|
+
)
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
suffix = tool_name.removeprefix(f"{candidate_name}_")
|
|
360
|
+
self._tool_name_counts[candidate_name] = int(suffix)
|
|
311
361
|
self._diagnostics.append(
|
|
312
|
-
f"Duplicate tool name '{candidate_name}' deduped to '{
|
|
362
|
+
f"Duplicate tool name '{candidate_name}' deduped to '{tool_name}'."
|
|
313
363
|
)
|
|
314
|
-
return deduped_name
|
|
315
364
|
|
|
316
365
|
def _process_parameters(
|
|
317
366
|
self, parameters: list[dict[str, Any]], input_schema: dict[str, Any]
|
|
@@ -399,7 +448,7 @@ class Mapper:
|
|
|
399
448
|
return None
|
|
400
449
|
|
|
401
450
|
primary_content_type: str | None = None
|
|
402
|
-
body_schema_openapi: dict[str, Any] | None = None
|
|
451
|
+
body_schema_openapi: dict[str, Any] | bool | None = None
|
|
403
452
|
primary_media: dict[str, Any] | None = None
|
|
404
453
|
|
|
405
454
|
if "application/json" in content and isinstance(
|
|
@@ -419,7 +468,7 @@ class Mapper:
|
|
|
419
468
|
primary_content_type,
|
|
420
469
|
)
|
|
421
470
|
|
|
422
|
-
if primary_content_type and isinstance(body_schema_openapi, dict):
|
|
471
|
+
if primary_content_type and isinstance(body_schema_openapi, (bool, dict)):
|
|
423
472
|
body_schema_json = openapi_schema_to_json_schema(
|
|
424
473
|
body_schema_openapi, self.spec, raise_on_error=True
|
|
425
474
|
)
|
|
@@ -469,7 +518,7 @@ class Mapper:
|
|
|
469
518
|
candidate_name = operation.get("operationId") or generate_tool_name(
|
|
470
519
|
method, path
|
|
471
520
|
)
|
|
472
|
-
tool_name = self.
|
|
521
|
+
tool_name = self._resolve_unique_tool_name(candidate_name)
|
|
473
522
|
description = build_tool_description(method, path, operation)
|
|
474
523
|
|
|
475
524
|
input_schema: dict[str, Any] = {
|
{openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/mapping/output_schema.py
RENAMED
|
@@ -117,6 +117,13 @@ def _supports_structured_output(schema: dict[str, Any]) -> bool:
|
|
|
117
117
|
schema_type = schema.get("type")
|
|
118
118
|
if schema_type == "object":
|
|
119
119
|
return True
|
|
120
|
-
if schema_type
|
|
120
|
+
if isinstance(schema_type, list):
|
|
121
|
+
return bool(schema_type) and all(item == "object" for item in schema_type)
|
|
122
|
+
if isinstance(schema_type, str):
|
|
121
123
|
return False
|
|
122
|
-
|
|
124
|
+
if "properties" in schema:
|
|
125
|
+
return True
|
|
126
|
+
all_of = schema.get("allOf")
|
|
127
|
+
return isinstance(all_of, list) and any(
|
|
128
|
+
isinstance(item, dict) and _supports_structured_output(item) for item in all_of
|
|
129
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Evaluate Swagger compatibility against policy-adjusted tool metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from openapi_to_mcp.policy.applier import apply_policy
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from openapi_to_mcp.common.spec_compatibility import SwaggerFinding
|
|
11
|
+
from openapi_to_mcp.policy.models import PolicyConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def effective_swagger_findings(
|
|
15
|
+
findings: list[SwaggerFinding],
|
|
16
|
+
tool: dict[str, Any],
|
|
17
|
+
policy: PolicyConfig | None,
|
|
18
|
+
) -> list[SwaggerFinding]:
|
|
19
|
+
"""Keep findings that remain relevant after tool selection and auth overrides."""
|
|
20
|
+
if not findings or policy is None:
|
|
21
|
+
return findings
|
|
22
|
+
selected = apply_policy([tool], policy)
|
|
23
|
+
if not selected:
|
|
24
|
+
return []
|
|
25
|
+
if _has_supported_security_metadata(selected[0]):
|
|
26
|
+
return [
|
|
27
|
+
finding
|
|
28
|
+
for finding in findings
|
|
29
|
+
if finding.field not in {"security", "securityDefinitions"}
|
|
30
|
+
]
|
|
31
|
+
return findings
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_supported_security_metadata(tool: dict[str, Any]) -> bool:
|
|
35
|
+
requirements = tool.get("_original_security")
|
|
36
|
+
if requirements is None or requirements == []:
|
|
37
|
+
return True
|
|
38
|
+
schemes = tool.get("_original_security_schemes")
|
|
39
|
+
if not isinstance(requirements, list) or not isinstance(schemes, dict):
|
|
40
|
+
return False
|
|
41
|
+
return all(
|
|
42
|
+
isinstance(requirement, dict)
|
|
43
|
+
and all(_runtime_supports_scheme(schemes.get(name)) for name in requirement)
|
|
44
|
+
for requirement in requirements
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _runtime_supports_scheme(scheme: object) -> bool:
|
|
49
|
+
"""Match the scheme types handled by the generated auth resolver."""
|
|
50
|
+
if not isinstance(scheme, dict):
|
|
51
|
+
return False
|
|
52
|
+
scheme_type = str(scheme.get("type", "")).lower()
|
|
53
|
+
if scheme_type == "http":
|
|
54
|
+
return str(scheme.get("scheme", "")).lower() == "bearer"
|
|
55
|
+
if scheme_type == "apikey":
|
|
56
|
+
location = scheme.get("in")
|
|
57
|
+
name = scheme.get("name")
|
|
58
|
+
return (
|
|
59
|
+
isinstance(location, str)
|
|
60
|
+
and location.lower() in {"header", "query", "cookie"}
|
|
61
|
+
and isinstance(name, str)
|
|
62
|
+
and bool(name.strip())
|
|
63
|
+
)
|
|
64
|
+
return scheme_type in {"oauth2", "openidconnect"}
|
|
@@ -87,7 +87,7 @@ class SchemaConverter:
|
|
|
87
87
|
|
|
88
88
|
def convert(
|
|
89
89
|
self,
|
|
90
|
-
openapi_schema:
|
|
90
|
+
openapi_schema: object,
|
|
91
91
|
*,
|
|
92
92
|
include_internal_markers: bool = False,
|
|
93
93
|
) -> dict[str, Any]:
|
|
@@ -101,6 +101,8 @@ class SchemaConverter:
|
|
|
101
101
|
Returns:
|
|
102
102
|
The converted JSON Schema.
|
|
103
103
|
"""
|
|
104
|
+
if isinstance(openapi_schema, bool):
|
|
105
|
+
return {} if openapi_schema else {"not": {}}
|
|
104
106
|
if not isinstance(openapi_schema, dict):
|
|
105
107
|
logger.debug(
|
|
106
108
|
"Invalid schema input provided to converter (expected dict). Returning empty schema."
|
|
@@ -177,7 +179,7 @@ class SchemaConverter:
|
|
|
177
179
|
|
|
178
180
|
|
|
179
181
|
def openapi_schema_to_json_schema(
|
|
180
|
-
openapi_schema:
|
|
182
|
+
openapi_schema: object,
|
|
181
183
|
full_spec: dict[str, Any],
|
|
182
184
|
*,
|
|
183
185
|
raise_on_error: bool = False,
|
{openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/array_schema.py
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from openapi_to_mcp.schema.handlers.base import SchemaHandler
|
|
6
|
+
from openapi_to_mcp.schema.handlers.base import SchemaHandler, schema_has_type
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
@@ -13,10 +13,9 @@ class ArraySchemaHandler(SchemaHandler):
|
|
|
13
13
|
|
|
14
14
|
def can_handle(self, schema: dict[str, Any]) -> bool:
|
|
15
15
|
"""Check if this handler can process the given schema."""
|
|
16
|
-
schema_type = schema.get("type")
|
|
17
16
|
# Handle explicit type=array or schemas with 'items'
|
|
18
17
|
return isinstance(schema, dict) and (
|
|
19
|
-
|
|
18
|
+
schema_has_type(schema, "array") or "items" in schema
|
|
20
19
|
)
|
|
21
20
|
|
|
22
21
|
def handle(
|
{openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/base.py
RENAMED
|
@@ -8,6 +8,14 @@ logger = logging.getLogger(__name__)
|
|
|
8
8
|
INTERNAL_CYCLIC_REFERENCE_MARKER = "_is_cyclic_reference"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def schema_has_type(schema: dict[str, Any], expected_type: str) -> bool:
|
|
12
|
+
"""Return whether a schema permits the expected JSON Schema type."""
|
|
13
|
+
schema_type = schema.get("type")
|
|
14
|
+
return schema_type == expected_type or (
|
|
15
|
+
isinstance(schema_type, list) and expected_type in schema_type
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
class SchemaConverterProtocol(Protocol):
|
|
12
20
|
"""Protocol defining the interface for SchemaConverter.
|
|
13
21
|
|
|
@@ -17,7 +25,7 @@ class SchemaConverterProtocol(Protocol):
|
|
|
17
25
|
|
|
18
26
|
def convert(
|
|
19
27
|
self,
|
|
20
|
-
openapi_schema:
|
|
28
|
+
openapi_schema: object,
|
|
21
29
|
*,
|
|
22
30
|
include_internal_markers: bool = False,
|
|
23
31
|
) -> dict[str, Any]:
|
{openapi_to_mcp_cli-0.9.2 → openapi_to_mcp_cli-0.9.3}/openapi_to_mcp/schema/handlers/composition.py
RENAMED
|
@@ -29,12 +29,12 @@ class CompositionHandler(SchemaHandler):
|
|
|
29
29
|
for comp_key in ["allOf", "oneOf", "anyOf", "not"]:
|
|
30
30
|
comp_value = openapi_schema.get(comp_key)
|
|
31
31
|
|
|
32
|
-
if comp_key == "not" and isinstance(comp_value, dict):
|
|
32
|
+
if comp_key == "not" and isinstance(comp_value, (bool, dict)):
|
|
33
33
|
# 'not' applies to a single schema
|
|
34
34
|
json_schema[comp_key] = self.converter.convert(comp_value)
|
|
35
35
|
elif comp_key != "not" and isinstance(comp_value, list):
|
|
36
36
|
# Others apply to an array of schemas
|
|
37
|
-
valid_items = [s for s in comp_value if isinstance(s, dict)]
|
|
37
|
+
valid_items = [s for s in comp_value if isinstance(s, (bool, dict))]
|
|
38
38
|
if valid_items:
|
|
39
39
|
json_schema[comp_key] = [
|
|
40
40
|
self.converter.convert(s) for s in valid_items
|