Skip to main content

spec2openapi

Convert legacy API specifications — SOAP/WSDL and Swagger 2.0 — into FastMCP-ready OpenAPI 3.x documents.

CI PyPI Python License: Apache-2.0 Code of Conduct

한국어 문서 (Korean README)


MCP (Model Context Protocol) tooling such as FastMCP can turn an OpenAPI 3.x document into an MCP server automatically — but enterprises are full of services described only by WSDL or Swagger 2.0. spec2openapi closes that gap:

WSDL ─────────┐
              ├──(spec2openapi)──> OpenAPI 3.x (+ x-soap extensions) ──> FastMCP.from_openapi() ──> MCP tools
Swagger 2.0 ──┘

The two inputs produce two kinds of output — this distinction matters:

  • Swagger 2.0 → a plain, standard OpenAPI 3.x document. The paths are the real REST endpoints. Any OpenAPI-driven runtime (FastMCP, or your own httpx-based server) serves it with zero runtime changes — just point it at the converted spec.
  • WSDL → OpenAPI 3.x + an x-soap contract. The generated /operations/... paths are not real REST endpoints; each tool call must be serialized to a SOAP envelope, sent to the SOAP endpoint, and the XML response parsed back to JSON. That logic is not part of a standard OpenAPI runtime — it lives in the SOAP bridge shipped in the [mcp] extra. Serving a SOAP-converted spec with a plain OpenAPI/httpx runtime will POST JSON to the SOAP endpoint and fail every call.

So spec2openapi is a converter for Swagger 2.0, and a converter + runtime contract (with a reference bridge) for SOAP. See How SOAP calls work below.

The fixed-runtime deployment model — build one image, swap the spec via a Kubernetes ConfigMap to mass-produce MCP servers — applies to both, as long as the image includes the [mcp] extra when serving SOAP specs.

Features

  • WSDL → OpenAPI 3.0/3.1 — document/literal and rpc/literal bindings, SOAP 1.1/1.2, nested complex types, arrays, attributes, nillable, inheritance (flattened complexContent extensions), simpleContent (text value + attributes), choice (members become optional + x-soap-choice), substitution groups (a head reference becomes a oneOf of self-describing member branches + x-soap-substitution), default values, recursive types, multi-service/multi-port WSDLs with automatic dedup.
  • XSD facets & docs carried into tool schemas — enumerations, pattern, length and numeric bounds, fractionDigits (→ multipleOf), and xsd:annotation documentation are extracted (including from xsd:import-ed schemas), plus deterministic example values (the first enumeration value; canonical date/time formatting illustrations) so LLMs see well-described, well-constrained tool arguments.
  • x-soap contract — SOAPAction, SOAP version, endpoint, wrapper element QNames, soap:header parts and declared faults are embedded as vendor extensions; OpenAPI xml annotations carry everything a call layer needs to serialize JSON ↔ literal XML.
  • Swagger 2.0 → OpenAPI 3.x upgrade — full mechanical mapping (servers, requestBody, formData/multipart, parameter schema wrapping, collectionFormatstyle/explode, $ref rewriting, security schemes, type: file, x-nullable, discriminator), hardened against real-world documents: deep local $refs are hoisted to components, dangling refs and duplicate parameters are neutralized, type-mismatched defaults are coerced, and common vendor extensions (x-example, x-oneOf, x-anyOf) are promoted to native keywords. Every assumption made for missing information is recorded in x-s2o.assumptions; untranslatable constructs are preserved as x- extensions and listed in x-s2o.lossy.
  • Real OpenAPI 3.1 output--openapi-version 3.1 is a semantic conversion to JSON Schema 2020-12 style (nullabletype arrays, boolean exclusiveMinimum/exclusiveMaximum → numeric bounds), not a version-string bump.
  • FastMCP compatibility, guaranteed and verifiable — operationIds are generated in FastMCP's tool-name alphabet ([A-Za-z0-9_], unique, ≤64 chars) so tool name == operationId. spec2openapi validate proves it: static checks, openapi-spec-validator, and a real FastMCP.from_openapi() round-trip listing the resulting tools.
  • SOAP bridge — required to serve SOAP specspip install "spec2openapi[mcp]" adds the bridge (custom httpx transport) that implements the x-soap contract, plus FastMCP glue, a fixed Dockerfile, and Kubernetes examples. SOAP faults map to MCP tool errors. Swagger-converted (pure REST) specs do not need this — any OpenAPI runtime serves them. Only SOAP-converted specs require the bridge at runtime.

Installation

Requires Python 3.10+. (On an older interpreter, pip reports No matching distribution found because every release is filtered out by the version floor.)

pip install spec2openapi          # converter + CLI (zeep, lxml, PyYAML)
pip install "spec2openapi[mcp]"   # + SOAP bridge & runtime — required to serve SOAP specs

The core install is enough to convert any spec and to serve Swagger-converted (REST) specs from your own runtime. The [mcp] extra is required only to serve SOAP-converted specs (it provides the bridge that turns JSON tool calls into SOAP envelopes).

Quick start

CLI

# See what a WSDL contains (operations, headers, faults, style)
spec2openapi inspect https://legacy-host/OrderService?wsdl

# WSDL -> OpenAPI (also accepts a zip bundle or '-' for stdin)
spec2openapi convert https://legacy-host/OrderService?wsdl -o orders.openapi.yaml
spec2openapi convert vendor-bundle.zip -o orders.openapi.yaml
spec2openapi convert - < service.wsdl

# Swagger 2.0 -> OpenAPI 3.x (assumptions reported on stderr)
spec2openapi upgrade swagger2.json -o service.openapi.yaml

# Prove the spec converts cleanly into MCP tools
spec2openapi validate orders.openapi.yaml

# Reference MCP runtime (requires the [mcp] extra)
spec2openapi serve orders.openapi.yaml --transport http --port 8000
$ spec2openapi validate orders.openapi.yaml
operations        : 2
component schemas : 3
openapi-spec-validator: OK
FastMCP round-trip: OK (2 tools)
  - CreateOrder(customer, items, note)
  - GetOrder(orderId)

OK: spec is FastMCP-convertible

Library

import spec2openapi
from spec2openapi import ConversionError

# Swagger 2.0 -> OpenAPI dict (input may be a path or an http(s) URL)
try:
    legacy = spec2openapi.load_spec("swagger2.json")
    spec = spec2openapi.convert_swagger(legacy, openapi_version="3.1")
except ConversionError as exc:      # every failure path raises this
    raise SystemExit(f"conversion failed: {exc}")

# everything the converter assumed or could not translate, per document
report = spec.get("x-s2o", {})
report.get("assumptions", [])       # e.g. "missing consumes -> application/json"
report.get("lossy", [])             # e.g. "collectionFormat 'tsv' preserved as x-"
# pipelines that must not accept guesses: convert_swagger(legacy, strict=True)

# the FastMCP-readiness contract as a function (empty list == ready)
problems = spec2openapi.check_fastmcp_ready(spec)

# WSDL -> OpenAPI dict (zeep loads on first SOAP use, not at import)
spec = spec2openapi.convert_wsdl(
    "https://legacy-host/OrderService?wsdl",
    forbid_external=True,           # refuse remote imports from untrusted WSDLs
)

# WSDL input is flexible: a path/URL, a zip bundle, the document itself,
# or an in-memory multi-file bundle whose relative imports resolve locally
spec = spec2openapi.convert_wsdl(content=wsdl_bytes)
spec = spec2openapi.convert_wsdl(files={"service.wsdl": wsdl_bytes,
                                        "types.xsd": xsd_bytes})

print(spec2openapi.dump_spec(spec))            # YAML text (fmt="json" for JSON)

# Optional [mcp] extra: run it as an MCP server right away
mcp = spec2openapi.from_openapi_spec(spec)
mcp.run(transport="http", host="0.0.0.0", port=8000)

The public API is exactly spec2openapi.__all__ (typed, PEP 561); anything else is internal and may change without notice. All entry points report failures as ConversionError (a ValueError subclass). Returned documents may share substructures with the input mapping (example/default/enum and x-* values are not deep-copied) — copy.deepcopy the result before mutating it if you keep using the input.

How SOAP calls work (the x-soap contract)

The generated paths (/operations/...) are not real REST endpoints — a SOAP translation layer must build the actual call. Everything it needs ships inside the spec:

Field (paths.*.post.x-soap) Meaning
operation / service / port WSDL names
soapAction, soapVersion, style "1.1"/"1.2", document/rpc
endpoint soap:address (override at runtime)
input / output wrapper element QNames
headers[] soap:header parts with schema refs
faults[] declared faults with schema refs

Serialization rules (schema xml annotations): xml.name/xml.namespace (absent namespace = unqualified), xml.attribute: true, xml.x-text: true (simpleContent text), arrays repeat the element, and property order = XSD sequence order (do not alphabetize the document). x-soap-choice lists mutually exclusive property groups. x-soap-substitution marks a substitution-group value: the JSON is a self-describing single-key object ({"creditCard": {…}}) and the wire carries that member element itself — the head element never appears.

The [mcp] extra contains a verified implementation of this contract (src/spec2openapi/bridge.py) — use it directly (via spec2openapi serve) or as the reference for your own runtime. There is no way to serve a SOAP-converted spec without an implementation of this contract; a standard OpenAPI runtime cannot do it.

Mixed SOAP + REST specs. The reference runtime routes all traffic through the SOAP bridge if any path carries x-soap, so REST operations in a mixed spec are not served correctly today. Keep SOAP and REST specs separate until this is addressed (tracking issue).

Handling missing information (Swagger 2.0)

Upgrading is favorable: OpenAPI 3.x is a superset of Swagger 2.0, so almost nothing must be invented. Where documents are genuinely underspecified, a three-tier policy applies:

  1. Deterministic, documented defaults — missing consumes/producesapplication/json; missing operationId{method}_{path}; missing host → relative server /; missing schemeshttps. All recorded in x-s2o.assumptions.
  2. Preserve, never drop — constructs with no OpenAPI 3 equivalent (e.g. collectionFormat: tsv) are kept as x- extensions and listed in x-s2o.lossy.
  3. Verify the outcomespec2openapi validate runs the actual FastMCP round-trip; assumptions never block tool generation because tools only need paths and schemas.

Pipelines that must not accept guessed conversions can pass --strict to upgrade (or strict=True to convert_swagger): the conversion then fails with the full list of assumption/lossy records instead of applying them.

Kubernetes: one image, many MCP servers

docker build -t spec2openapi:0.2.2 .
spec2openapi convert <wsdl> -o openapi.yaml
kubectl create configmap my-mcp-spec --from-file=openapi.yaml
kubectl apply -f k8s/example.yaml    # Deployment mounts /config/openapi.yaml

Only the ConfigMap changes per service; credentials live in a Secret (SPEC2OPENAPI_ENDPOINT, SPEC2OPENAPI_AUTH = basic|wsse, SPEC2OPENAPI_USERNAME/PASSWORD, SPEC2OPENAPI_TIMEOUT, SPEC2OPENAPI_VERIFY, SPEC2OPENAPI_TRUST_ENV). The MCP endpoint is http://<service>:8000/mcp (streamable HTTP).

Limitations

rpc/encoded (skipped and recorded in x-soap.skippedOperations), MTOM/attachments, and WS-Policy/WS-Addressing are not supported. Substitution-group blocking/final constraints are ignored. WS-Security support in the reference runtime is UsernameToken (PasswordText).

Security

All XML parsing disables DTD loading, entity resolution, and parser-level network access. When converting WSDLs from untrusted sources, add --forbid-external (CLI) or forbid_external=True (API) to refuse fetching remote wsdl:/xsd: imports (SSRF mitigation; local relative imports still work). See SECURITY.md for the full notes and how to report vulnerabilities.

Development

git clone https://github.com/Seo-yul/spec2openapi.git
cd spec2openapi
pip install -e ".[dev]"
python -m pytest tests/

The suite covers conversion units, the Swagger upgrader, envelope (de)serialization, end-to-end MCP-tool-call → mock-SOAP-server round-trips (rpc, simpleContent, choice, recursive trees, unqualified forms), FastMCP round-trips for every fixture × OpenAPI 3.0/3.1, and stress patterns (circular $refs, deep nesting, large enums, cross-namespace name collisions, duplicate operation names across services, odd path characters, deep allOf chains). Generated samples live in examples/.

An opt-in corpus sweep additionally runs the Swagger upgrader over a stratified sample of real-world Swagger 2.0 definitions from the public APIs.guru directory (fetched at test time, cached locally, never committed), checking every output against openapi-spec-validator (3.0 and 3.1) and a live FastMCP.from_openapi() round-trip:

python -m pytest -m corpus     # network required; see tests/corpus/

Known failures would be tracked in tests/corpus/known_failures.txt with issue links so the sweep only fails on regressions — the list is currently empty: the full APIs.guru Swagger 2.0 population (975 testable documents) was swept during the 0.3.0 cycle, every converter defect it surfaced was fixed, and every sampled document passes all three oracles.

Project layout

src/spec2openapi/
  parser.py    WSDL parsing (zeep) + raw XSD scraping (facets/docs)
  schema.py    XSD -> JSON Schema (xml annotations, choice, simpleContent)
  openapi.py   OpenAPI 3.0/3.1 assembly + x-soap extensions
  swagger.py   Swagger 2.0 -> OpenAPI 3.x upgrader (x-s2o report)
  convert.py   core public API
  cli.py       convert / upgrade / inspect / validate / serve
  bridge.py    [mcp] SOAP bridge (httpx transport)
  server.py    [mcp] FastMCP glue

Contributing

Contributions are welcome — see CONTRIBUTING.md. This project follows the Contributor Covenant Code of Conduct; by participating you agree to uphold it. Security issues should be reported privately per SECURITY.md.

License

Apache-2.0 © Seoyul Yoon

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spec2openapi-0.4.0.tar.gz (89.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spec2openapi-0.4.0-py3-none-any.whl (60.8 kB view details)

Uploaded Python 3

File details

Details for the file spec2openapi-0.4.0.tar.gz.

File metadata

  • Download URL: spec2openapi-0.4.0.tar.gz
  • Upload date:
  • Size: 89.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for spec2openapi-0.4.0.tar.gz
Algorithm Hash digest
SHA256 105f735279c6ccf45ab417167a6e0a9cc1ec492727dff9cccdd0f9d13b4ea340
MD5 b185c99acd29092328424ea81922ed0b
BLAKE2b-256 2861c36d11c41b2dcdfe58644044805a7ede475dee5df4e8f4f4eb4ff2da8657

See more details on using hashes here.

File details

Details for the file spec2openapi-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: spec2openapi-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 60.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for spec2openapi-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c91e0ddf3806d8db48471224d6634aca2f0108ff99bd164f09db2138b0d67799
MD5 a4cbf5e33544e92ae2ca14258a1a4268
BLAKE2b-256 6c3623ca832632103d7ce5caacc40ede1ae9dfd17213b413ad9f92519cebd4b5

See more details on using hashes here.

Release history Release notifications | RSS feed

0.6.0

2 files

0.5.0

2 files

This release

0.4.0 This release

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page