legacy2mcp
legacy2mcp turns a legacy SOAP/WSDL service, database, or REST API into a working MCP server in 60 seconds. Read-only by default, with every call audit-logged.
Contents
- Install
- Quick start
- How it works
- Why not just write an adapter by hand?
- Point it at your own WSDL
- Point it at your own database
- Point it at your own REST API
- Generate a config automatically
- Common commands
- Client support
- What's handled
- Safety model
- Data flow and privacy
- Use cases
- Real-world usage
- Configuration reference
- Documentation
- Roadmap
- Development · Contributing · License
Install
# Full install (recommended)
pip install "legacy2mcp[ai]"
# Or the identical, scan-only-ready core -- AI-assisted `legacy2mcp init`
# needs no extra package either way, just an API key at runtime; `[ai]`
# above installs nothing extra today, it's just a stable, familiar name
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
No API key, no cloning — against a public demo API:
pip install legacy2mcp
legacy2mcp init --rest-url "https://petstore3.swagger.io/api/v3"
Run that from a real terminal and it opens a local test-chat page in your
browser automatically — click one of the example questions and see a real
response come back from the live API. No AI key needed for this part; it's
local keyword matching against the tools it just discovered, just enough
to sanity-check that the generated server actually works. (Prefer the raw
tool list instead? legacy2mcp inspect --config config.yaml.)
Next, connect it to your AI client for real — see Client support.
Have your own WSDL, REST API, or codebase instead? legacy2mcp init (no
flags) walks you through it — see
Generate a config automatically. More
demos (a local SOAP service, Docker, a WSDL served by raw URL) are listed in
examples/.
How it works
flowchart LR
WSDL["WSDL / XSD"] --> GEN["legacy2mcp<br/>schema generation"]
DB["DB schema<br/>(allowlisted tables)"] --> GEN
GEN --> TOOLS["Typed MCP tools<br/>one per operation / table / query"]
AGENT["AI agent /<br/>MCP client"] -->|tool call| VAL{"schema<br/>validation"}
TOOLS -. defines .-> VAL
VAL -->|invalid args| REJ["rejected, never<br/>reaches SOAP/DB"]
VAL -->|valid and allowed| BACKEND["SOAP endpoint<br/>or database"]
BACKEND --> RESP["plain JSON<br/>back to the agent"]
VAL --> LOG[("audit log")]
SOAP:
- 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 useadditionalProperties: false), calls the operation viazeep, serializes the response to plain JSON, and writes an audit entry.
DB:
- Connects with the dialect's driver (
psycopg/pyodbc/oracledb, imported lazily) and introspects the live column types for each allowlisted table (adapters/db_dialects.py) into JSON Schema (schema/sql_to_jsonschema.py). - Registers
select_<table>/insert_<table>per allowlisted table-and-operation, plusquery_<name>per named query in config. - On a tool call: validates arguments, builds SQL from the allowlisted table/columns (or runs the config-supplied named query) using one dialect-neutral
:paramplaceholder convention, executes it, and writes an audit entry. The LLM never sees or writes SQL text. - Operations whose names look like writes are excluded unless
allow_write_operations: true.
REST:
- There's no live schema to introspect a plain REST API from the way there is a WSDL or a DB catalog, so the endpoint list (method, path, typed params) comes from config -- hand-written, or generated by
legacy2mcp init's OpenAPI/Swagger discovery or source-code scan. - Registers one MCP tool per allowlisted endpoint, named
<adapter_id>_<name>. - On a tool call: validates arguments, splits them into path/query/body by each param's declared
location, substitutes{param}placeholders into the URL, and calls it withrequests-- auth applied per the adapter'sauthconfig (basic/bearer/none). - Endpoints marked
write: true(non-GETby default) are excluded unlessallow_write_operations: true.
Why not just write an adapter by hand?
Because a hand-written adapter is a second copy of your system's schema that nobody remembers to update. The first time a WSDL operation's signature shifts or a column changes, your tool's JSON Schema silently drifts from reality — and an LLM calling it finds out by sending arguments that are wrong in a way nothing catches, or by getting a confusing runtime error from deep inside the SOAP/DB client. legacy2mcp builds the schema from the live system every time it starts, so a drifted call fails at validation, before it ever reaches your backend — not after.
| Hand-written adapter (FastMCP / raw SDK) | Generic OpenAPI-to-MCP proxy | legacy2mcp | |
|---|---|---|---|
| Schema source | Typed by hand, per tool | The OpenAPI spec, if one exists | The live WSDL / DB schema, or a discovered OpenAPI spec |
| Can drift from the real system | Yes — nothing re-checks it | Only if the spec itself is stale | No — rebuilt from the live source at startup |
| Covers SOAP / a database | You write that client code yourself | No — REST/OpenAPI only | Yes, plus REST |
| Write operations hidden by default | No — you remember to gate each one | No — usually exposes everything the spec defines | Yes — explicit allow_write_operations opt-in |
| Audit log of every call | DIY | Usually no | Built in |
| Time to a working server | Hours to days, per system | Minutes, for a REST API with a spec | Minutes — legacy2mcp init |
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.
Point it at your own database
pip install "legacy2mcp[db-postgres]" # or db-mssql / db-oracle
# config.yaml
adapters:
- id: orders
type: db
config:
dialect: postgres # postgres | mssql | oracle
dsn_env: "ORDERS_DB_DSN" # connection string, read from the environment
allow_write_operations: false # insert_<table> / write: true queries stay hidden
tables:
- name: orders
operations: [select]
columns: [id, status, total, created_at] # column allowlist
filterable_columns: [status] # equality filters on select_orders
max_rows: 200
queries:
- name: orders_by_status_since
sql: "SELECT id, status, total FROM orders WHERE status = :status AND created_at >= :since"
params:
- { name: status, type: string, required: true }
- { name: since, type: string, required: true }
export ORDERS_DB_DSN=...
legacy2mcp inspect --config config.yaml # review the generated tools
Columns are introspected from the live schema at startup, the same way SOAP operation
schemas come from the WSDL -- there's nothing to hand-maintain. There is no path from
the LLM to arbitrary SQL, ever: it can only call select_<table> / insert_<table> for
tables listed under tables:, or a query_<name> tool whose SQL you wrote under
queries: -- the LLM supplies only the placeholder values a query's params declare,
never SQL text. A production-shaped template lives at
examples/db/config.template.yaml.
Point it at your own REST API
There's no WSDL or live database schema to introspect a plain REST/JSON API
from, so the rest adapter's endpoint list is explicit config -- write it
by hand, or generate a starting point with legacy2mcp init (next
section).
# config.yaml
adapters:
- id: orders
type: rest
config:
base_url: "https://api.example.com"
auth:
type: bearer
token_env: "ORDERS_API_TOKEN"
allow_write_operations: false
endpoints:
- name: get_order
method: GET
path: "/orders/{order_id}"
params:
- { name: order_id, location: path, type: integer, required: true }
- name: create_order
method: POST
path: "/orders"
write: true # stays hidden unless allow_write_operations: true
params:
- { name: total, location: body, type: number, required: true }
A production-shaped template lives at
examples/rest/config.template.yaml.
Generate a config automatically
legacy2mcp init asks what to point it at, then writes a starting
config.yaml for you:
legacy2mcp init
What do you want to point legacy2mcp at?
1) WSDL URL (SOAP service)
2) REST API URL (tries OpenAPI/Swagger discovery)
3) GitHub repo URL or a local path to its source code
Choice [1-3]:
| Option | What happens | Needs an LLM key? |
|---|---|---|
| WSDL URL | Points a soap adapter at it directly -- zeep introspects the live WSDL at inspect/run time anyway, so nothing needs scanning up front. |
No |
| REST API URL | Tries the common OpenAPI/Swagger spec locations (/openapi.json, /swagger.json, ...) against that URL and builds the rest adapter's endpoint list from the real spec if one's found; an empty scaffold otherwise. |
No |
| GitHub repo / local path | Clones (or reads) the repo and statically scans its Python source (AST only, never executed) for a zeep.Client(...) call, a SQLAlchemy/Flask-SQLAlchemy model, or Flask/FastAPI routes -- picking soap/db/rest accordingly. |
Optional |
Each also works non-interactively via a flag -- --wsdl-url, --rest-url,
--github-url, or --local-path -- for scripting or CI. Only the GitHub/
source-code path can use an LLM, and only to refine what static analysis
already found (ambiguous parameter types, read-vs-write classification) --
never a dump of your repo. It needs no extra dependency, just an
OpenAI-compatible API key via --llm-api-key-env, or an interactive hidden
prompt if you omit that (never written to disk); pass --scan-only to skip
it outright and run static-analysis-only, same as not entering a key.
legacy2mcp init --github-url https://github.com/you/your-api --scan-only --out config.yaml
Whichever path you take, review the generated file before running it --
it's marked up with # TODO comments for anything that couldn't be
confidently inferred (an unresolved base_url, an unverified param type,
which endpoints got classified write: true), and nothing in it is
verified against the real backend until you do:
legacy2mcp inspect --config config.yaml # review the generated tools
legacy2mcp run --config config.yaml # start the MCP server
legacy2mcp init also writes a plain-English HTML report next to the
config (e.g. config.yaml -> config.html) -- open it in a browser to see,
in sentences rather than YAML, what tools the server will expose (every
REST endpoint / DB table / SOAP target it found, which ones can change data
and are gated behind allow_write_operations, and the review items above).
Pass --html-out to change where it's written, or --no-html-report to
skip it.
Run init from a real terminal against a config that's already usable
(a live --wsdl-url/--rest-url, or a scan that found something concrete)
and it also opens a local test-chat page in your browser, with a
clickable example question under every discovered tool (generated from
that tool's own name and schema, e.g. getPetById -> "get pet by id 4") so
you don't have to guess what to type. Click one, or type your own, and it
matches it to a discovered tool, calls the real adapter, and shows you the
real response. No AI/API key needed for this -- it's local keyword
matching against the tool names/descriptions, not an LLM, just enough to
sanity-check that the server actually works before you wire up a real MCP
client. It's skipped automatically when the config still has TODOs to fill
in (nothing to connect to yet), and never opens at all in
non-interactive/scripted runs. Pass --no-chat to opt out.
See docs/security.md for exactly what this heuristic does and doesn't guarantee before pointing it at anything real.
Common commands
# Generate a config from a live REST API (tries OpenAPI/Swagger discovery)
legacy2mcp init --rest-url "https://api.example.com" --out config.yaml
# Generate a config from a WSDL URL directly -- no scan needed
legacy2mcp init --wsdl-url "https://service.example.com/Legacy?wsdl" --out config.yaml
# Generate a config by statically scanning a GitHub repo (no AI, no API key)
legacy2mcp init --github-url https://github.com/you/your-api --scan-only --out config.yaml
# Same, with AI-assisted refinement of ambiguous parameter types / read-vs-write
legacy2mcp init --github-url https://github.com/you/your-api --llm-api-key-env OPENAI_API_KEY --out config.yaml
# Review the tools a config generates -- no server started
legacy2mcp inspect --config config.yaml
# Print the tool list as JSON, for scripting or a CI diff check
legacy2mcp inspect --config config.yaml > tools.json
# Start the MCP server (stdio)
legacy2mcp run --config config.yaml
# Teach the AI coding agent working in *this* repo how to drive legacy2mcp --
# writes/updates CLAUDE.md-style context files (Claude Code, Cursor, AGENTS.md,
# GEMINI.md, GitHub Copilot). Safe to re-run; review and commit what it writes.
legacy2mcp install-skill
Client support
"Verified" here means a specific, narrow thing: the exact command + config
shape shown below was actually spawned and completed a real MCP
initialize handshake over stdio -- not just "MCP should be compatible."
For MCP Inspector, the loop was driven further: a real tools/list, a real
tools/call, and the resulting audit log entry, all
headlessly via the Inspector's own --cli mode. The last badge is what
keeps that claim honest over time rather than a one-time snapshot: a CI job
(mcp-compatibility.yml) re-runs
that same tools/list / tools/call / audit-log loop on every push and
fails the build if the server can't start or exposes the wrong tools. Every
client below talks to legacy2mcp over stdio only -- there's no
HTTP/SSE transport yet (it's on the roadmap, not
shipped), so an http://localhost:... MCP URL won't work here.
| Client | Status | How it was verified |
|---|---|---|
| Claude Desktop | Verified | Spawned the exact claude_desktop_config.json command, completed a real initialize handshake, confirmed tools listed |
| Cursor | Verified | Same, via .cursor/mcp.json's exact command |
| VS Code | Verified | Same, via .vscode/mcp.json's exact command |
| Claude Code | Documented | Uses the identical launch command verified above via claude mcp add; that command itself wasn't independently re-run in this environment |
| MCP Inspector | Tested | Full CLI loop -- tools/list, tools/call, and the audit log entry it produced |
This repo also ships the checked-in configs themselves --
.mcp.json, .cursor/mcp.json, and
.vscode/mcp.json -- pointed at the bundled Petstore
demo, so opening this repo in one of these tools gets you a working server
with no setup. Copy whichever matches your tool into your own project and
point --config at your own config.yaml.
Claude Desktop — claude_desktop_config.json
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"legacy2mcp": {
"command": "uvx",
"args": ["legacy2mcp", "run", "--config", "/absolute/path/to/config.yaml"]
}
}
}
If you installed with pip/pipx instead of uv, use "command": "legacy2mcp" and drop the
first element of args.
Cursor — .cursor/mcp.json
Project-local (.cursor/mcp.json) or global (~/.cursor/mcp.json):
{
"mcpServers": {
"legacy2mcp": {
"command": "uvx",
"args": ["legacy2mcp", "run", "--config", "/absolute/path/to/config.yaml"]
}
}
}
VS Code — .vscode/mcp.json
{
"servers": {
"legacy2mcp": {
"type": "stdio",
"command": "uvx",
"args": ["legacy2mcp", "run", "--config", "/absolute/path/to/config.yaml"]
}
}
}
Claude Code — CLI
claude mcp add legacy2mcp -- uvx legacy2mcp run --config /absolute/path/to/config.yaml
Add --scope project to write a shared .mcp.json for your team (keep secrets out of it);
check with claude mcp list.
MCP Inspector — a validation tool, not an AI client
npx @modelcontextprotocol/inspector uvx legacy2mcp run --config /absolute/path/to/config.yaml
Opens a local web UI: connect to the server, confirm tools are listed, run one read-only tool,
and check legacy2mcp-audit.log for the matching entry. That exact loop -- tool discovery, one
real call, the audit log it produces -- is what the "Tested with MCP Inspector" badge above
is actually backed by, driven headlessly through the Inspector's own --cli mode rather than
just opened in a browser.
Passing secrets and troubleshooting: docs/clients.md.
What's handled
| Area | Covered |
|---|---|
| Type mapping | string / int / long / decimal / boolean / date / dateTime / base64Binary / … → JSON Schema types + formats |
| Structure | nested complex types, repeated elements → arrays, xsd:enumeration → enum, recursion depth-limited |
| Discovery | 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); bearer token (*_env); anonymous |
| Transport | stdio (the transport Claude Desktop and most agent frameworks spawn) |
| DB dialects | Postgres, SQL Server, Oracle — table allowlist (select/insert) with live schema introspection, plus named parameterized queries |
| REST | explicit endpoint allowlist (method + path + typed params) from config or legacy2mcp init — no live schema to introspect, so nothing here is verified until you run it |
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/DB layer without passing jsonschema.validate against that tool's generated schema. |
| Read-only by default | SOAP: operation names are matched against write-verb prefixes (Create, Update, Delete, Cancel, Void, Submit, Pay, …). DB: tables/queries are explicit opt-in, and insert/write-marked queries additionally require it. Neither is exposed unless you set allow_write_operations: true. |
| Explicit allow / deny | SOAP: include_operations / exclude_operations. DB: a table/column allowlist — there's no equivalent of "all tables" or arbitrary SQL. |
| Audit log | One JSON line per call — tool, arguments, timestamp, outcome, duration. |
| Secret hygiene | Credentials and DB connection strings come from named environment variables; the YAML stays safe to commit. |
Data flow and privacy
- No telemetry. legacy2mcp collects no usage data and phones home to nothing. Every network call it makes is one you configured it to make — verified by checking every outbound HTTP call site in the source before writing this sentence.
- Adapter calls go exactly where you pointed them, nowhere else. SOAP calls go to your
wsdl_url, DB calls go to yourdsn_envconnection, REST calls go to yourbase_url. legacy2mcp init's discovery calls are scoped to what you gave it. The WSDL and REST(OpenAPI) paths only ever contact the URL you passed. The GitHub/source-code path clones the repo you specify and parses it locally withast— nothing from it is ever executed.- The optional LLM-assist step sends only already-detected, ambiguous fragments — never your repo. It only runs on the GitHub/source-code path of
legacy2mcp init, only if you provide an API key, and only sends the specific route names/paths/params the static scan already flagged as ambiguous, not a dump of your source. Skip it entirely with--scan-only. - Secrets never live in
config.yaml. Passwords, DSNs, and API tokens are all referenced by environment-variable name (password_env,dsn_env,token_env) and read at runtime — the YAML stays safe to commit. - The audit log stays local and is not encrypted at rest. Every tool call is appended as one JSON line to
security.audit.path. Arguments are logged as-is — there's no PII redaction; see docs/security.md for the full list of what the audit log does and doesn't do.
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:
- 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
Exit codes:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
A config, source, or runtime error — bad WSDL/DB/REST config, a repo that couldn't be cloned, a missing required environment variable, and so on |
2 |
Invalid command-line arguments (argparse's own default) |
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
| Key | Default | Meaning |
|---|---|---|
server.name |
legacy2mcp |
MCP server name reported to clients |
server.transport |
stdio |
only stdio is implemented so far |
adapters[].id |
— | prefix for this adapter's tool names |
adapters[].type |
— | soap, db, or rest (implemented); queue is on the roadmap |
SOAP adapter config:
| Key | Default | Meaning |
|---|---|---|
adapters[].config.wsdl_url |
— | WSDL location (http(s)://…?wsdl or a file path) |
adapters[].config.auth |
{type: none} |
none or {type: basic, username, password_env} |
adapters[].config.allow_write_operations |
false |
expose write-like operations |
adapters[].config.include_operations |
all | allowlist of operation names |
adapters[].config.exclude_operations |
[] |
denylist of operation names |
adapters[].config.timeout_seconds |
15 |
per-call SOAP timeout |
DB adapter config:
| Key | Default | Meaning |
|---|---|---|
adapters[].config.dialect |
— | postgres, mssql, or oracle |
adapters[].config.dsn_env |
— | env var holding the connection string/DSN |
adapters[].config.allow_write_operations |
false |
expose insert_<table> and write: true queries |
adapters[].config.timeout_seconds |
15 |
connection timeout |
adapters[].config.tables[].name / .schema |
— | table (and optional schema/owner) to allowlist |
adapters[].config.tables[].operations |
[select] |
select and/or insert |
adapters[].config.tables[].columns |
all introspected | column allowlist |
adapters[].config.tables[].filterable_columns |
[] |
columns usable as equality filters on select_<table> |
adapters[].config.tables[].max_rows |
200 |
row cap, enforced after the query runs |
adapters[].config.queries[].name / .sql / .params |
— | a named query; sql uses :param_name placeholders |
adapters[].config.queries[].write |
false |
gates this query behind allow_write_operations |
REST adapter config:
| Key | Default | Meaning |
|---|---|---|
adapters[].config.base_url |
— | base URL the configured endpoints are relative to |
adapters[].config.auth |
{type: none} |
none, {type: basic, username, password_env}, or {type: bearer, token_env} |
adapters[].config.allow_write_operations |
false |
expose endpoints marked write: true |
adapters[].config.timeout_seconds |
15 |
per-call HTTP timeout |
adapters[].config.endpoints[].name / .method / .path / .description |
— | one allowlisted endpoint; path uses {param} placeholders for path params |
adapters[].config.endpoints[].params[].name / .location / .type / .required |
— | location is path, query, or body |
adapters[].config.endpoints[].write |
false |
gates this endpoint behind allow_write_operations |
Security config:
| Key | Default | Meaning |
|---|---|---|
security.audit.enabled |
true |
write the audit log |
security.audit.path |
./legacy2mcp-audit.log |
audit log location |
Documentation
| Doc | What it covers |
|---|---|
| docs/security.md | The full, honest security model — what's validated and audited today, and what isn't yet |
| docs/roadmap.md | Shipped vs. planned, version by version |
| docs/clients.md | Connecting every supported MCP client, secrets, troubleshooting |
| docs/releasing.md | The PyPI / MCP Registry release process |
| docs/registry-submissions.md | Registry / awesome-list submission status and reusable copy |
| examples/README.md | Every runnable demo config (SOAP, REST, Docker) |
| llms.txt | A machine-readable project summary for LLMs and agents |
| skill/legacy2mcp/ | A SKILL.md for coding agents (Claude Code, Cursor, Copilot, ...) — teaches an agent when and how to drive legacy2mcp itself |
| CHANGELOG.md | Notable changes, release by release |
| CONTRIBUTING.md | Dev setup, how to add an adapter, PR expectations |
Roadmap
| Version | Scope | Status |
|---|---|---|
| v0.1 | SOAP/WSDL adapter, schema generation, validation, read-only default, audit log, run + inspect CLI, basic auth, stdio |
✅ shipped |
| v0.2 | Database adapter (Postgres / SQL Server / Oracle; table/operation allowlists + named parameterized queries, no arbitrary SQL ever) | ✅ shipped |
| v0.2.x | REST adapter (explicit endpoint allowlist); legacy2mcp init -- scans a repo (SOAP client / ORM models / Flask/FastAPI routes) and generates a starter config, with optional OpenAI-compatible LLM assist for ambiguous bits |
✅ shipped |
| v0.2.x | HTTP/SSE transport, role→tool authorization, OAuth2 for SOAP | planned |
| v0.3+ | 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
pip install -e ".[dev]"
pytest tests/ -v
Tests run against an in-process mock SOAP service and an in-process SQLite-backed
stand-in for the DB adapter — no network, no real Postgres/SQL Server/Oracle
required. The DB adapter's Postgres/SQL Server/Oracle dialects (db_dialects.py)
have their SQL-translation logic covered by driver-free unit tests; connecting to
a real instance of each is not part of the default suite.
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. See CONTRIBUTING.md for dev setup, how adapters are structured, and PR expectations. This project follows the Code of Conduct. Issues and PRs welcome.
License
Metadata
Release files for legacy2mcp 0.2.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| legacy2mcp-0.2.6.tar.gz | 102.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| legacy2mcp-0.2.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 175.3 kB
Release files / legacy2mcp-0.2.6.tar.gz
| Download URL | legacy2mcp-0.2.6.tar.gz |
|---|---|
| Size | 102.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3c31ad190f3f4bfc30cabe64f9dd2813725d52ad44b121a89f7544f168afcc9a
|
|
BLAKE2b-256 checksum How to use checksums |
d1b348c4516c20234e653365279911b7c31fb65a219fca8be3344dea51a86d91
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / legacy2mcp-0.2.6-py3-none-any.whl
| Download URL | legacy2mcp-0.2.6-py3-none-any.whl |
|---|---|
| Size | 73.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9019ea74585caf9bf5d1496d7e80c54754c69eee31e40e5c3d65afc0ca1f646a
|
|
BLAKE2b-256 checksum How to use checksums |
46dd73c1f65ff0447af42b2882c6fac92f5abe178a05e1bfd70aad7cf197de16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log