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.
  • Structured verification (verify) — a library API that runs 21 checks over a converted document (Swagger- and WSDL-converted alike): document shape, MCP tool-name rules (SEP-986), tool descriptions, the full x-soap contract, and — with the optional deps installed — openapi-spec-validator plus the FastMCP in-memory round-trip. Returns a deterministic, JSON-serializable report in which every check carries normative citations and "could not check" (skip) is always distinguishable from "checked and passed". verify never raises; spec2openapi validate --format json exposes the same report on the CLI.
  • MCP tool-payload minification (optional)minify_for_mcp() shrinks and enriches what FastMCP actually sends the model: foreign vendor extensions are stripped from schema subtrees, and error responses / payload examples — which the MCP tool shape otherwise drops — can be folded into tool descriptions deterministically. Serving behavior is unchanged. See Minifying the LLM-facing surface.
  • 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
spec2openapi validate orders.openapi.yaml --format json   # machine-readable report

# 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)

# structured verification: the same contract plus the x-soap checks,
# openapi-spec-validator, and a FastMCP in-memory round-trip
result = spec2openapi.verify(spec)   # Swagger- and WSDL-converted alike
result.ok          # no failed checks
result.complete    # nothing skipped (deep deps installed, so they ran)
result.to_dict()   # JSON-serializable report with per-check citations

# 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.

Minifying the LLM-facing surface (optional)

When a spec is served over MCP, the model never reads the OpenAPI document. FastMCP sends the tool list: name, description, inputSchema, and (FastMCP 3.x) an outputSchema built from the 2xx response schema. Everything else — info, unused components, error responses, media-type examples, the $ref structure — stays on the server. That has two costs: schema content is copied into tool schemas verbatim, so machine-to-machine vendor extensions burn context tokens in every tools/list; and human-authored facts with no slot in the tool shape (error responses, request/response examples) silently vanish.

minify_for_mcp addresses both directions as an optional post-processing step — the conversion pipeline itself is unchanged:

spec = spec2openapi.convert_swagger(legacy)          # or convert_wsdl(...)
spec = spec2openapi.minify_for_mcp(spec, enrich=("errors", "examples"))
  • Default: foreign vendor x-* extensions are removed from schema subtrees (the locations FastMCP copies into tool payloads). Everything a runtime or a reader needs survives: x-soap* and xml annotations (SOAP bridge), x-s2o, x-fastmcp-*, the project's preservation extensions (x-pattern, x-collectionFormat), and documentation-bearing extensions (x-enum-varnames, x-example, ...). Protect your own with keep_extensions=("x-acme-*",).
  • enrich: "errors" folds error responses into the description (Errors: 404 (not found); ... — they never reach the model otherwise); "examples" folds request/response payload examples (Example request: {...}) and hoists parameter-level example values into the parameter schema, where FastMCP actually shows them. Only facts already in the document are used — nothing is invented, and re-runs never duplicate a fold.
  • Opt-in trims: max_description=N caps the descriptions that reach the payload (truncated text is marked with ); drop_value_examples=True strips in-schema examples — they usually help the model format arguments, so measure before enabling.

For any option combination the output is a valid OpenAPI document, still passes check_fastmcp_ready, and serves identically — no option touches the callable surface, and SOAP bridge envelopes are byte-identical. What was removed or folded is summarized under x-s2o.minify. Minification is one-way: write the result to a separate file and keep the original.

Large specs: control the tool count with route maps

minify_for_mcp trims each tool, but on a large service the dominant context cost is the number of tools: every operation becomes one, and MCP clients load the entire tools/list result into the model's context on every request. Measured on examples/, one tool payload is roughly 0.3–1.7 KB — a 100-operation service ships tens of KB before the conversation starts, and a model choosing among 100 tools misfires more often than one choosing among 5. An agent usually needs a handful.

Choosing which operations become tools is the OpenAPI→MCP layer's job, and FastMCP already owns it with route maps. from_openapi_spec forwards extra keyword arguments to FastMCP.from_openapi, so route maps pass straight through — and the SOAP bridge keeps working, because excluded operations simply never receive a call:

from fastmcp.server.providers.openapi import MCPType, RouteMap

mcp = spec2openapi.from_openapi_spec(
    spec,
    route_maps=[
        RouteMap(tags={"OrderService"}, mcp_type=MCPType.TOOL),  # keep these
        RouteMap(mcp_type=MCPType.EXCLUDE),                      # drop the rest
    ],
)

WSDL-converted specs tag every operation with its service name, so service-per-agent subsets need no extra tagging. The reference CLI (spec2openapi serve) does not expose route maps — use the Python entry point when you need a subset.

Kubernetes: one image, many MCP servers

docker build -t spec2openapi:0.6.0 .
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
  checks.py    structured verification (verify / VerifyReport)
  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.6.0.tar.gz (122.0 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.6.0-py3-none-any.whl (79.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: spec2openapi-0.6.0.tar.gz
  • Upload date:
  • Size: 122.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for spec2openapi-0.6.0.tar.gz
Algorithm Hash digest
SHA256 7b654bc0bc890912d7efa5f7472b3ffe12fb60bbcfb017c0931d9695091ac2e2
MD5 cd196ef012f216c18ac880ce400b9a66
BLAKE2b-256 dbace257181f9c2b8e470736c0690274b1936c78ff8a2ff61a2c69e427fd9633

See more details on using hashes here.

File details

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

File metadata

  • Download URL: spec2openapi-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 79.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for spec2openapi-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7ed5216e63823fa27b81184ad9d3a1ffa74bc2dce9c75a57f265ce563cc2592b
MD5 d9be959857206e8679277c6bb7249d3b
BLAKE2b-256 49ae8a3ddb8463cd58ef7d7e91798c144d739a17651d34cf71fb40a7c82588ca

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

0.5.0

2 files

0.4.0

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