Turn a legacy SOAP/WSDL service into a safe, typed, schema-validated, audit-logged MCP server
The io.github.bvenkata/legacy2mcp Model Context Protocol (MCP) server converts a legacy SOAP/WSDL service into a safe, typed MCP server. It targets schema validation and audit logging, enabling AI agents to call the service without a hand-written adapter.
Point it at a WSDL. Get an MCP server whose tools can't drift from the service, can't send unvalidated arguments, and can't call write operations you didn't opt into.
legacy2mcp turning a Calculator WSDL into four typed, schema-validated MCP tools
NOTE
legacy2mcp introspects every operation in a WSDL, builds a real JSON Schema for each one from the WSDL's own XSD types, and exposes them as MCP tools β with every call schema-validated before it reaches your SOAP endpoint, write-like operations excluded by default, and every call audit-logged. No hand-written adapter code, no hand-maintained schemas.
Also published to the MCP Registry as io.github.bvenkata/legacy2mcp, so registry-aware MCP clients can discover it directly.
Quick start
Try it end-to-end against the bundled mock SOAP service β no external network, no real backend:
bash
git clone https://github.com/bvenkata/legacy2mcp.git
cd legacy2mcp
pip install -e ".[dev]"# 1. start the demo SOAP service (dneonline-style Calculator WSDL)
python examples/soap/run_mock_calculator.py &
# 2. see the MCP tools generated from its WSDL
legacy2mcp inspect --config examples/soap/config.calculator.yaml
Or with Docker
bash
docker compose up demo-soap-service -d
docker compose run --rm legacy2mcp legacy2mcp inspect \
--config examples/soap/config.calculator.docker.yaml
How it works
flowchart LR
WSDL["WSDL / XSD"] --> GEN["legacy2mcp<br/>schema generation"]
GEN --> TOOLS["Typed MCP tools<br/>one per operation"]
AGENT["AI agent /<br/>MCP client"] -->|tool call| VAL{"schema<br/>validation"}
TOOLS -. defines .-> VAL
VAL -->|invalid args| REJ["rejected, never<br/>reaches SOAP"]
VAL -->|valid and allowed| SOAP["SOAP endpoint"]
SOAP --> RESP["plain JSON<br/>back to the agent"]
VAL --> LOG[("audit log")]
Loads the WSDL with zeep, a mature, widely-used Python SOAP client.
For every operation on every port/binding, converts the XSD input type into a JSON Schema (schema/xsd_to_jsonschema.py) β simple types, nested complex types, enums and arrays, recursively, depth-limited for pathological WSDLs.
Registers one MCP tool per operation, named <adapter_id>_<OperationName>.
On a tool call: validates arguments with jsonschema (schemas use additionalProperties: false), calls the operation via zeep, serializes the response to plain JSON, and writes an audit entry.
Operations whose names look like writes are excluded unless allow_write_operations: true.
Point it at your own WSDL
yaml
# config.yamlserver:name:my-legacy-mcpadapters:-id:legacytype:soapconfig:wsdl_url:"https://service.example.com/LegacyService?wsdl"auth:type:basicusername:"svc-account"password_env:"SERVICE_PASSWORD"# value read from the environment, never the fileallow_write_operations:false# Create*/Update*/Delete*/β¦ stay hiddeninclude_operations: ["GetRecord", "GetRecordDetails", "SearchRecords"]
security:audit:enabled:truepath:"./legacy-mcp-audit.log"
bash
export SERVICE_PASSWORD=...
legacy2mcp inspect --config config.yaml # review the generated tools
legacy2mcp run --config config.yaml # start the MCP server (stdio)
every service β port β binding β operation; duplicate tool names rejected at startup
Invocation
argument validation, zeep call, response serialized to plain JSON, single-field responses re-wrapped to a named result
Errors
SOAP faults and transport errors caught and returned as clean messages β no stack traces to the caller
Auth
HTTP basic (username + *_env password); anonymous
Transport
stdio (the transport Claude Desktop and most agent frameworks spawn)
See docs/security.md for the full, honest security model β what's covered today and what isn't yet.
Safety model
Layer
What it does
Schema validation
No arguments reach the SOAP layer without passing jsonschema.validate against that operation's generated schema.
Read-only by default
Operation names are matched against write-verb prefixes (Create, Update, Delete, Cancel, Void, Submit, Pay, β¦); those tools aren't exposed unless you set allow_write_operations: true.
Explicit allow / deny
include_operations (allowlist) and exclude_operations (denylist) on top of the heuristic.
Audit log
One JSON line per call β tool, arguments, timestamp, outcome, duration.
Secret hygiene
Credentials come from named environment variables; the YAML stays safe to commit.
WARNING
The write-operation filter is a name heuristic, not semantic analysis β an operation called ProcessRecord that deletes data would not be caught. For any system where a wrong call has real consequences, set include_operations explicitly and don't rely on the heuristic. There is also no auth/authz on the MCP server itself yet β don't expose a v0.1 server to untrusted callers. Details in docs/security.md.
Use cases
Domain
Shape
Systems of record
An agent reads status/detail records from a legacy back-office platform, read-only, every lookup logged.
Financial services
Expose account and transaction reads without exposing transfers or adjustments.
Supply chain / ERP
Surface order status, inventory, shipment tracking from an old SOAP middleware layer.
Internal support tooling
A support copilot gets safe, typed access to the system of record instead of a scraped UI.
Migration & modernization
Put an MCP layer in front of a legacy service now; swap the backend later without touching the agent.
Real-world usage
In CI/CD β catch WSDL drift before it reaches production
legacy2mcp inspect loads the config, contacts the WSDL, builds every schema, and exits non-zero if anything fails:
legacy2mcp run speaks MCP over stdio. Package it with your config using the provided Dockerfile and let your MCP client launch it.
In a data pipeline
Call the same generated, validated tools from your own code via any MCP client library to pull records on a schedule β the audit log records exactly what was fetched.
Queue adapter (Kafka/RabbitMQ/SQS), workflow composition with approval gates, OpenTelemetry export
ideas
Full detail in docs/roadmap.md. The BaseAdapter interface (discover_tools() + invoke()) is the extension point β the server core handles validation, dispatch and audit for any adapter.
Development
bash
pip install -e ".[dev]"
pytest tests/ -v # runs against an in-process mock SOAP service β no network
CI runs the suite on Python 3.10β3.12 (ci.yml). Releases to PyPI and the MCP Registry are tag-triggered β see docs/releasing.md. The demo GIF is regenerated with vhs demo/demo.tape (demo/).
Contributing
Adapters for new legacy systems are the highest-value contribution β implement BaseAdapter and the core handles the rest. Issues and PRs welcome.