legacy2mcp

Turn a legacy SOAP/WSDL service into a safe, typed, schema-validated, audit-logged MCP server

OtherPythonv0.1.0
legacy2mcp — legacy SOAP/WSDL turned into safe, typed MCP tools

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.

Contents

Install

pip install legacy2mcp
# or:  uv tool install legacy2mcp   ·   pipx install legacy2mcp

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:

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
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")]
  1. Loads the WSDL with zeep, a mature, widely-used Python SOAP client.
  2. 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.
  3. Registers one MCP tool per operation, named <adapter_id>_<OperationName>.
  4. 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.
  5. Operations whose names look like writes are excluded unless allow_write_operations: true.

Point it at your own WSDL

# config.yaml
server:
  name: my-legacy-mcp

adapters:
  - id: legacy
    type: soap
    config:
      wsdl_url: "https://service.example.com/LegacyService?wsdl"
      auth:
        type: basic
        username: "svc-account"
        password_env: "SERVICE_PASSWORD"   # value read from the environment, never the file
      allow_write_operations: false        # Create*/Update*/Delete*/… stay hidden
      include_operations: ["GetRecord", "GetRecordDetails", "SearchRecords"]

security:
  audit:
    enabled: true
    path: "./legacy-mcp-audit.log"
export SERVICE_PASSWORD=...
legacy2mcp inspect --config config.yaml   # review the generated tools
legacy2mcp run     --config config.yaml   # start the MCP server (stdio)

A production-shaped template with comments lives at examples/soap/config.template.yaml.

Use it from Claude Desktop (or any MCP client)

{
  "mcpServers": {
    "legacy": {
      "command": "legacy2mcp",
      "args": ["run", "--config", "/absolute/path/to/config.yaml"]
    }
  }
}

What's handled

AreaCovered
Type mappingstring / int / long / decimal / boolean / date / dateTime / base64Binary / … → JSON Schema types + formats
Structurenested complex types, repeated elements → arrays, xsd:enumerationenum, recursion depth-limited
Discoveryevery service → port → binding → operation; duplicate tool names rejected at startup
Invocationargument validation, zeep call, response serialized to plain JSON, single-field responses re-wrapped to a named result
ErrorsSOAP faults and transport errors caught and returned as clean messages — no stack traces to the caller
AuthHTTP basic (username + *_env password); anonymous
Transportstdio (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

LayerWhat it does
Schema validationNo arguments reach the SOAP layer without passing jsonschema.validate against that operation's generated schema.
Read-only by defaultOperation 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 / denyinclude_operations (allowlist) and exclude_operations (denylist) on top of the heuristic.
Audit logOne JSON line per call — tool, arguments, timestamp, outcome, duration.
Secret hygieneCredentials 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

DomainShape
Systems of recordAn agent reads status/detail records from a legacy back-office platform, read-only, every lookup logged.
Financial servicesExpose account and transaction reads without exposing transfers or adjustments.
Supply chain / ERPSurface order status, inventory, shipment tracking from an old SOAP middleware layer.
Internal support toolingA support copilot gets safe, typed access to the system of record instead of a scraped UI.
Migration & modernizationPut 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:

- name: Check the WSDL still generates valid MCP tools
  env:
    SERVICE_PASSWORD: ${{ secrets.SERVICE_PASSWORD }}
  run: |
    pip install legacy2mcp
    legacy2mcp inspect --config config/legacy.yaml > tools.json
    git diff --exit-code --no-index tools/legacy.snapshot.json tools.json  # optional: pin the contract

As a sidecar / long-running MCP server

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.

Configuration reference

KeyDefaultMeaning
server.namelegacy2mcpMCP server name reported to clients
server.transportstdioonly stdio is implemented in v0.1
adapters[].idprefix for this adapter's tool names
adapters[].typesoap (implemented); db / queue are on the roadmap
adapters[].config.wsdl_urlWSDL location (http(s)://…?wsdl or a file path)
adapters[].config.auth{type: none}none or {type: basic, username, password_env}
adapters[].config.allow_write_operationsfalseexpose write-like operations
adapters[].config.include_operationsallallowlist of operation names
adapters[].config.exclude_operations[]denylist of operation names
adapters[].config.timeout_seconds15per-call SOAP timeout
security.audit.enabledtruewrite the audit log
security.audit.path./legacy2mcp-audit.logaudit log location

Roadmap

VersionScopeStatus
v0.1SOAP/WSDL adapter, schema generation, validation, read-only default, audit log, run + inspect CLI, basic auth, stdio✅ shipped
v0.2Database adapter (parameterized-query-only, table/operation allowlists), HTTP/SSE transport, role→tool authorization, OAuth2 for SOAPplanned
v0.3+Queue adapter (Kafka/RabbitMQ/SQS), workflow composition with approval gates, OpenTelemetry exportideas

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

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.

License

Apache 2.0

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
uvx legacy2mcp

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-bvenkata-legacy2mcp": {
      "command": "uvx",
      "args": [
        "legacy2mcp"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

legacy2mcppypi

Compatible MCP Clients

legacy2mcp works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More